RockyGuard

Blog

The signature is unforgeable. The branch that trusts it isn't.

·Rocky Software ·5 min read

engineering-notessecuritybuild-and-packaging

A RockyGuard licence is signed. Change one byte of the file — a date, a fingerprint, a feature flag — and the Ed25519 signature no longer verifies and load() returns SignatureInvalid. There is no known way to forge a valid licence without the vendor’s private key, and no amount of obfuscation is what protects that. The mathematics does.

So a serious attacker doesn’t attack the signature. They attack the code that reads the result.

if (!verify_signature(payload, sig))
    return SignatureInvalid;   // <- flip this

That is an ordinary conditional. It compiles to a compare and a jump, and a jump is one byte from becoming its opposite. The expiry check, the hardware-match threshold — same shape, same exposure. The attacker never forges anything; they find the branch and patch it. Our on-disk and in-memory integrity checks are built to detect that patch. This is the complementary move: make the branch hard to find in the first place.

What flattening and string encryption actually remove

Two techniques do most of the work, applied at the compiler’s intermediate representation rather than to source.

Control-flow flattening takes the natural shape of the decision code — the tidy tree of if/else a reverser reads like a map — and rewrites it into a single dispatch loop driven by a state variable. The branches are all still there; what’s gone is the structure that told an attacker which one to care about. Every block looks like every other block.

String encryption removes the other map. A binary full of "[RockyGuard] ERROR: System clock…" and .lck_cache_ hands a reverser a search box: grep for the message, land on the check. Encrypt those constants and the strings aren’t in the binary to grep — they’re assembled at runtime, right before use.

We apply this to exactly one translation unit: the licence-decision code, and nothing else. Flattening the crypto and the fingerprint-gathering would cost real performance and make the library miserable to debug, for no security gain — those aren’t the branch anyone patches. The scope is the point.

We had to build the compiler first

The obvious tool was a well-known LLVM obfuscator. It supports only ARM — it’s built for mobile — so on an x86-64 desktop binary it does precisely nothing, silently. The one from the same family that does target x86-64 ships no prebuilt binaries any more. So we built it from source.

That turned out to be the right posture anyway, not a detour. The whole exercise is about not trusting the shipped binary to an attacker; running an untrusted third-party compiler over the licence code to get there would be an odd way to begin. A toolchain you compiled yourself is one you can actually trust with the code that matters.

Two things that differ across the platforms we ship

Getting it working on the second platform surfaced the kind of detail these notes exist for.

The flag that turns a pass on is passed to the compiler driver, and the two drivers we use disagree about how. The Windows driver needs each token wrapped in a prefix; the Linux driver takes it bare. This is not cosmetic: hand one driver the other’s spelling and the flags are accepted, ignored, and the build succeeds unobfuscated. A protection that silently does nothing is worse than no protection, because you think you have it. The build system now picks the right spelling per driver so that failure mode can’t happen.

The second: the obfuscator’s own source didn’t compile under the Linux host compiler — a narrowing conversion that two compilers accept and a third rejects. A three-line fix, carried as a patch beside the build scripts. Cross-platform work is mostly this: not the big idea, but the third compiler that has opinions.

It stacks; it doesn’t replace

Three layers now sit on the same attack. The signature makes the licence unforgeable. The integrity check detects a patch to the decision code — on disk, and in memory. Obfuscation makes that patch harder to write by hiding where to write it. Each covers a different move; none is load-bearing alone.

We measured it rather than assumed it. The full test suite passes against the obfuscated build on both platforms — behaviour and ABI are identical, because this is a build-time transform of one file, not an API change. The give-away strings are gone from the object. The decision code grows between roughly two and four times in size, which is the flattening and the encrypted constants showing up as instructions. A normal build with the option off is byte-for-byte what it was.

The ceiling, stated plainly

This runs on the attacker’s machine, and that sentence is the whole limit. Any offline licence check can, with enough skill and time, be broken on hardware the attacker fully controls. Obfuscation raises the cost and the skill floor, and it defeats the one-click and scripted cracks that make up most of the actual threat. It does not make the check a wall, and we would rather say so than imply otherwise.

There’s a more useful thing to say, though, and it’s about your side of the boundary, not ours. The most effective attack on a licensed application is usually not against the licence library at all:

if (result.status == LicenseStatus::Valid)
    unlock_all_features();     // one branch, in YOUR binary

If everything hangs on one gate at startup, an attacker patches that if, in your executable, and never touches RockyGuard. The strongest library in the world sits behind it, untouched and irrelevant. Check the result in more than one place; let the outcome feed real program logic instead of a single boolean; sign your own executable so the OS notices when someone edits it. The obfuscated decision code is a hardened lock — worth having, and worth mounting on a door that isn’t held shut by a single screw.


It’s off by default: an opt-in build option, so a plain rebuild is unchanged. The customer-facing summary is Customer Documentation §9.8; if reverse-engineering of the shipped binary is in your threat model, ask us about an obfuscated build for your platform. The download page has the demo bundle.