Writing

Reverse engineering a "Ghost" client

reverse-engineer a video game cheat and emulate it.

I have always wondered how certain cheats are able to work in popular games and servers. I ended up reverse-engineering a fast block-break module from the popular "ghost" client Drip Lite, it's a very well-made and intricate cheat client.

The basic working model

Just like how I E2E or replicate production issues at work, I applied a similar pattern:

  1. Start by capturing data. For example, you might place slog.info calls or use Prometheus and Grafana. I used a logging plugin for the server.
  2. Capture data from the cheat module, then capture the vanilla mechanics as a baseline.
  3. Analyse the data manually or with an agent. The difference was more than obvious once I had the agent filter it and output a few tables.
  4. Create a reproduction "harness" -- in this case, a mod that reproduces what the client does.

The underlying game mechanics

The module only changes two client values, both on MultiPlayerGameMode: the per-tick destroyProgress accumulation and the post-break destroyDelay. The vanilla start/finish packet flow is unchanged -- the only timing shift is that progress crosses the break threshold earlier, so the finish packet leaves the client sooner.

What the break-lab arena traces showed

I tested nine blocks in each run using an Efficiency I iron pickaxe, with no status effects. The JSONL traces recorded the packet metadata and matching server events for both the vanilla baseline and the modded client using a 1.15 multiplier with destroyDelay set to 0.

Signal Vanilla baseline Modded client What it showed
DIG_START / DIG_FINISH 9 / 9 9 / 9 One normal start and finish for every block; no packet burst.
Median arm packets per break 8 7 The modified client reached the break threshold with fewer progress ticks.
Median start-to-finish 440.33 ms 385.13 ms The finish packet left the client earlier.
Median finish-to-next-start 329.78 ms 54.75 ms Removing destroyDelay made the next block begin almost immediately.
Dig sequence step 1 1 Packet sequence IDs still advanced normally.

As you can see everything looks normal-ish you have to get really low level server-side and take into consideration ping-compensation to create the inverse an anti-cheat check to measure for this

╭─〔 CLIENT MINING LOOP 〕────────────────────────────────────────╮
│                                                                 │
│  @ START_DESTROY_BLOCK ─────────────────> server                │
│  │                                                              │
│  ├─<> VANILLA                                                   │
│  │   ╰──> fn getDestroyProgress(...)                           │
│  │         progress += vanilla amount                           │
│  │          ├─ progress < 1.0  ↻ next tick                     │
│  │          ╰─ progress >= 1.0 ─────────> FINISH packet        │
│  │                                                              │
│  ╰─<> FAST-BREAK                                               │
│      ╰──> fn getDestroyProgress(...) x multiplier              │
│            progress += boosted amount                           │
│             ├─ progress < 1.0  ↻ next tick                      │
│             ╰─ progress >= 1.0 ────────> FINISH packet          │
│                                      ╰── destroyDelay := floor  │
│                                                                 │
╰─────────────────────────────────────────────────────────────────╯

                 〔 SERVER-SIDE OBSERVATION 〕

 START packet  ⟿  ⧖ elapsed ticks  ⟿  FINISH packet
                         │
                         ╰─ faster than legally possible ─────► ⚑

With a decent LLM, you can get this working in a mod compatible with any version. That was my main motivation -- getting the cheat out of the client and into a Fabric mod compatible with the latest version of the game.

Mod Menu showing the delay and multiplier controls

The mixin stuff

The whole reproduction is a single @Redirect in MultiPlayerGameModeMixin that scales the one BlockState.getDestroyProgress(...) call inside MultiPlayerGameMode.continueDestroyBlock, plus an @Accessor for the private destroyDelay field. The algorithm is tiny: multiply the per-tick progress and shorten the cooldown; the rest of the mining loop is untouched.

@Redirect(method = "continueDestroyBlock",
    at = @At(value = "INVOKE",
        target = "Lnet/minecraft/world/level/block/state/BlockState;"
               + "getDestroyProgress(Lnet/minecraft/world/entity/player/Player;"
               + "Lnet/minecraft/world/level/BlockGetter;Lnet/minecraft/core/BlockPos;)F"))
private float breakTheory$multiplyDestroyProgress(
        BlockState state, Player player, BlockGetter level, BlockPos pos) {
    float vanillaProgress = state.getDestroyProgress(player, level, pos);
    return BreakTheoryClient.shouldModify(player.getMainHandItem())
            ? (float) (vanillaProgress * BreakTheoryClient.config().multiplier())
            : vanillaProgress;
}

The accessor is the other half. It is the only reason the cooldown floor can be set externally, since destroyDelay is a private int on MultiPlayerGameMode:

@Mixin(MultiPlayerGameMode.class)
public interface MultiPlayerGameModeAccessor {
    @Accessor("destroyDelay") int breakTheory$getDestroyDelay();
    @Accessor("destroyDelay") void breakTheory$setDestroyDelay(int value);
}

BreakTheoryClient.applyConfiguredDelay, registered on ClientTickEvents.END_CLIENT_TICK, only writes when destroyDelay > config.delayTicks(), so the client's own transient state is not overridden.

Uses for this

Obviously, being able to break blocks faster than the vanilla game allows is useful. It helps on servers where breaking blocks is the purpose of the game mode, but I would caution against using it on multiplayer servers with good anti-cheats, as it is highly detectable by any half-thought-out game simulation check.

The same cycle of having the cheat, a server with packet-measuring observability and logging, and time to go through the data is useful. It is how you can develop anti-cheat checks, and it is largely how I have managed to find so many nasty production bugs that other, more intelligent-on-paper engineers might draw blanks at.

The same process in production

For example, I wrote a performance-mocking tool to emulate long-term usage of our product. We had a performance-scaling issue where, around eight hours in, we would see degraded performance. The tool mocked data by inserting lorem ipsum and all the other metadata needed to pretend the session had reached that length. This helped me find many performance issues.

A similar version, but for microservice outages, wrapped our Go binary/server with this great tool. It allowed me to find audio-stream resuming bugs that caused production outages.

It is trivial and makes sense, but spending time building a "tool" just to accurately emulate usage is really, really useful. Obviously, you have to go to greater depths for the emulation to be more realistic. The proxy tool is very close to a VM outage, but mocking eight hours of usage by inserting lorem ipsum into an internal browser memory structure is different. There is no real-world method to emulate eight hours of Chrome V8 GC spikes, console logging ETC -- chrome profile traces + giving them to the chrome built-in gemini works quite well too!

There is a bit of skill in knowing what is near the real world and what is close enough. Obviously, the best way to collect useful debugging data is through actual real-world production logging.

PostgreSQL QPS comparison from a separate performance investigation

This is why having Grafana and Prometheus hooked up can be useful for post-mortem investigations and for seeing the impact of new or old features. It is hard to convince some engineers of the usefulness of these tools, but I think I make my case: you get more information to correlate with errors, successes, and the like.