Why licence checks get patched, and what actually survives
Almost every licence check ever shipped ends in a conditional jump. That is the whole problem, and most of the ways people try to fix it do not fix it.
The shape of the problem
Here is the check almost everyone writes first. It does not matter what language you started in, because this is roughly what it becomes.
if (!license_is_valid()) {
show_nag();
exit(1);
}Compiled, that is a call, a test, and a branch:
call license_is_valid
test al, al
je .invalid ; jump if the check returned falseSomeone attacking this does not care how license_is_valid works. They find that branch and change it. Replace je with jne and the program now demands an invalid licence. Replace it with two nop bytes and the program never checks at all. Set al to 1 before the test and the function's entire body becomes decoration.
It does not matter that the function behind that call is two thousand lines of clever. The result of all that work is one bit, and one bit is cheap to forge.
Rung one: hide the check
The usual first response is obfuscation. Inline the check in forty places, encrypt the strings, add a virtual machine, check again on a timer.
This raises the cost and it genuinely does deter casual attempts. It also fails against anyone patient, for a reason that has nothing to do with how good the obfuscation is: the program has to tell the user when it refuses. That nag dialog, that log line, that silent exit all have to happen somewhere, and that somewhere is a breakpoint. You work backwards from the message to the branch that produced it. Forty checks just means forty branches, and a script can find the rest once you know the shape of the first.
Rung two: ask a server
So move the decision off the machine. The client asks your API, the API knows the truth, and the client cannot fake what it does not compute.
Except the client still has to act on the answer, and acting on it means turning the response into a branch. You have not removed the branch, you have added a network round trip in front of it. The attacker now has three options and all of them are easy: patch the branch as before, point the program at a server they control by editing one hostname in the binary or one line in a hosts file, or run a local proxy that answers every request with a cheerful yes.
Worth being clear about this one, because it is the most common misunderstanding: putting the check on a server does not help unless the client can tell your server's answer apart from anyone else's.
Rung three: sign the answer
This is the first rung that changes anything. The server signs every response, the client verifies the signature against a public key compiled into the binary, and now a proxy can read the traffic but cannot manufacture a valid yes. Forging one means forging a signature, which means having the private key, which is on your server.
Two details decide whether this works or is theatre.
The key must be in the binary. If the client fetches your public key at runtime, the same proxy that answers the licence call also answers the key request, hands over its own key, and signs everything with the matching private half. Fetching the key is the same as having no key.
The signature must cover something the client chose. Sign only the response and an attacker records one valid success and replays it forever. The usual fix is for the client to send a fresh random value with each request and for the server to echo it inside the signed body, so a replayed response carries the wrong one and gets rejected.
Do both and you have closed the network entirely. The attacker cannot forge, cannot replay, cannot impersonate. And it still does not save you, because the code doing the verifying belongs to them too. Verification ends in a branch. So does everything.
The thing every rung has in common
Every layer above is a question the program asks itself and then answers with yes or no. You can make the question harder to find, harder to fake, harder to replay. You cannot make it impossible to delete, because it runs on hardware the attacker owns, in a process they can debug, from a file they can rewrite.
A determined attacker with a disassembler and enough time wins this. Anyone telling you otherwise is selling something.
What actually changes the game
There is one move that is different in kind rather than degree: stop asking whether the program may run, and start making the program unable to run without something only your server can produce.
Concretely, ship part of what the software needs inside the licence response, encrypted, keyed to that session. Configuration the engine reads, a table it looks up, coefficients, whatever is real work in your product rather than a flag. In ours it is a blob sealed with AES-256-GCM under a key derived from the session's access token and the device's hardware hash, resealed on every refresh because the token rotates.
Now consider the attacker's usual move. They find the branch, they nop it, they run the program. The program starts, reaches the point where it needs that data, and does not have it. There is nothing to patch, because nothing is being asked. The bytes simply are not there, and no amount of editing the binary conjures them out of nowhere. To get them you need a real session, and a real session needs a real licence.
This is not magic and it has an obvious limit. Someone who buys one legitimate copy can capture the decrypted data once and bake it into a patched build. What it does is change the economics. Instead of one person flipping a byte and posting a two kilobyte patch, someone has to buy your software, extract the payload, and redistribute a much larger and much more traceable artifact. And when you ship an update with different sealed content, their build is dead and yours is not. The size of the payload and how often it changes are the two dials that matter.
What you are actually buying
Not prevention. Four things, all of which are worth money:
Cost. Most piracy is opportunistic. The gap between a one click patcher and a job that needs a real purchase plus reverse engineering removes most of it.
Time. Revenue is front loaded. A working bypass that lands in week six is worth much less to your attacker and much less costly to you than one that lands on day one.
Staleness. If your software needs the server periodically, patched builds rot. Revocation that propagates within a heartbeat means a shared licence stops working while the user is holding it, rather than at the next launch.
Evidence. Device history, address history, geography per key, and Windows builds watermarked per buyer so a leaked copy names the account it came from. This is the part people skip and then wish they had, because knowing which customer is redistributing your software is worth more than any check.
Three practical notes
Pin the signing key, not the TLS certificate. Certificate pinning breaks every time a certificate rotates, which behind any CDN is often, and it only stops casual proxying anyway since anyone patching the binary removes the pin too. Pinning the response signing key defends the thing that matters, which is whether the answer is genuine.
Assume the protocol is public. Ours is documented in full on this site. Anyone can read exactly how it works, and that is fine, because none of the security depends on them not knowing. A design that needs secrecy is one leak away from worthless.
Fail closed on your own bugs, open on your own outages. If the signature does not verify, refuse. If your server is unreachable, do not punish a paying customer for your downtime. A signed, short lived offline grace token covers the aeroplane and the broken connection without giving an attacker an easy way in.
The honest summary
You cannot stop someone determined, on hardware they control, with unlimited time. What you can do is make casual copying expensive, make patched builds go stale quickly, and know who leaked what. Aim at those and you will do well. Aim at unbreakable and you will spend a year on obfuscation that a bored teenager removes in an afternoon.
This is how we think about the problem at Licentry, which is a licensing API built around signed sessions, device bound tokens and watermarked builds. If you want the protocol, it is in the docs, and the client hardening notes are in the tamper resistance section.