# Why Code Protection Is Hard: The Challenges Every Protector Faces
Code protection is not a problem you solve. It is an arms race you survive. Threat actors, game cheaters, and software pirates have several advantages on their side when it comes to attacking software: time, a mature and freely available tooling ecosystem, a large community that openly shares knowledge and techniques, and most recently, Artificail Intelligence.
This raises challenges for those in the industry who seek to protect their intellectual property. The attacker needs to win once and the protector needs to win every time, against every tool, every reverse engineer, and every technique that did not exist when the protection was designed. This brings incredible technical barriers to entry that filter out most attempts before they ever reach the market.
Building a credible code protector demands competency across an unusually wide surface. A protector that works against managed .NET assemblies must understand the Common Language Runtime, the IL instruction set, metadata tables, and how decompilers like ILSpy and dnSpy reconstruct high-level code from raw bytecode. A protector that touches native binaries must understand PE structure, calling conventions, compiler-generated patterns, and how disassemblers like IDA Pro and Ghidra build their analysis graphs. A protector that operates at the driver or system DLL level must additionally contend with the Windows loader, PPL signing requirements, and the kernel’s own integrity enforcement stack. These are not overlapping skill sets. They are separate disciplines, each requiring years of dedicated work, and a protection engine that claims to cover all of them is making a significant engineering claim.
Beyond the technical depth, the market itself is unforgiving. A single false positive that corrupts a protected binary in production, a single edge case that crashes a customer’s application, or a single public bypass that goes unpatched destroys credibility in a community that moves fast and remembers everything. The barrier to entry is not just knowing how compilers work. It is building something correct enough, robust enough, and adaptive enough to survive contact with real software in the real world.
The challenges that follow set the terms of the race and define the difficulty of defending against them. It names the challenges, explains them, and shows how the Obelisk-Protector, our solution to this fundamental problem, holds up against each one with exact measurements.
Key takeaways
Transforming a binary without changing its behavior is the wall most protectors never get past, and it's where the true engineering feat lives.
AI has lowered the bar for attacking protected code, which raises the bar for protecting it. Obelisk-Protector's answer to the LLM assisted analysis starts by poisoning the decompiler output the model reads.
Open source obfuscators raise the floor but not the ceiling. Recognizable transforms, no correctness proof, and no response when a technique breaks. That gap is what an engine like Obelisk is built to close.
A reverse engineer has unlimited time
The position is simple. The person you are defending against has your binary on their own machine, with physical access, unlimited time, and no deadline. They are not racing a clock. They are not constrained by a budget. They can spin up a VM, attach a debugger, and work the problem for as long as it takes. You built the software once. They get to study it forever.
This asymmetry is not a flaw in the design of code protection. It is the definition of the problem. The code must reach the end user to do its job, which means it must also reach the attacker. Every transform, every layer, every technique you apply ships in the same package as the thing you are trying to hide. There is no trusted execution environment you can retreat to. There is no channel the attacker cannot observe. The binary is the attack surface, and it is fully in their hands.
The realistic goal is narrower than most marketing language admits. You are not trying to stop analysis. You are trying to price it. A protection scheme that costs a skilled analyst a week of work is adequate. One that costs months is exceptional. One that makes the return on investment negative — where the value of what is inside is less than the cost of getting to it — has solved the problem in the only way it can actually be solved.
Most protectors on the market do not clear even the first bar. Recognizable transforms, static keys, and textbook CFG flattening fall inside the first hour for any analyst who has seen them before. The floor is low. Raising the ceiling is the engineering problem.
The Rewrite Must Be Perfect
Every technique in this article shares one requirement nobody advertises. Each one is a rewrite of the binary, and the rewrite must not change what the program does. Rewriting x86 correctly means parsing variable-length instructions, relocations, exception tables, unwind information, and thread-local storage, then emitting a transformed file where every jump still lands, every exception still unwinds, and every thread still starts. One wrong byte is a crash for a paying customer, and a crash is a reason to disable the protection and ship without it. That correctness constraint is the quiet wall of this field. The analyst-facing transforms get the attention. The rewriting engine gets the blame when behavior changes.
The complications compound quickly. Exception handling alone is a project. On x64 Windows, structured exception handling is table-driven rather than stack-based, which means every function that can throw needs a corresponding entry in the .pdata section with accurate unwind codes. Move that function, extend it, or split it, and the unwind table must reflect the change exactly. Get it wrong and the program runs fine until an exception fires, at which point the stack unwinder walks off into garbage and the process dies. Thread-local storage carries similar traps. Relocations must be recalculated after every transform that shifts code. Import tables must stay intact or be rebuilt correctly. The list of things that can silently break under a naive rewrite is long, and most of them only surface under specific conditions that do not appear in basic testing.
Size is the other cost nobody puts in the marketing material. Themida and VMProtect are the two most recognized commercial protectors on the market, and both are infamous for what they do to binary size. A clean executable that ships at 2 MB routinely exits those tools at 8 to 15 MB or larger, depending on which features are enabled. The bloat comes from the virtual machine stubs, the handler tables, the dispatcher logic, and the support code the protector injects alongside the transformed binary. For desktop software that ships on a fast connection that overhead is annoying. For embedded targets, mobile builds, or anything with a strict size budget it can be a hard blocker. A rewriting engine that adds transforms without injecting a runtime interpreter keeps the size delta small, but it also gives up the strongest protection tier. That tradeoff has no clean answer, and every protector on the market has landed somewhere different on it.
It must run to be useful
The most obvious statement, and also the most difficult. This one ties directly into the binary rewriting problem, because the transforms that make a binary hard to read statically are the same transforms that must execute correctly at runtime without degrading performance or changing behavior.
A protected program that runs slowly is a protected program that gets uninstalled. Every transform added to a binary has a runtime cost. Opaque predicates add branch instructions. Control flow flattening adds a dispatcher loop that executes on every basic block transition. Virtualization replaces native instructions with interpreted bytecode, and interpretation is orders of magnitude slower than native execution. A game that drops from 120 frames per second to 60 after protection is applied is not a protected game. It is a broken one, and the developer will strip the protection before shipping rather than take the support burden.
The protector has to preserve not just correctness but performance characteristics that the end user can feel. That means the hot paths through the binary, the tight loops, the rendering code, the physics tick, the network handler, all of them need to exit the protection layer with overhead that stays below the threshold of perception. Identifying those paths, protecting everything else aggressively, and leaving the critical sections either untouched or protected only with transforms that carry negligible runtime cost is one of the hardest profiling problems in the field. Most protectors apply transforms uniformly and leave the performance tuning to the developer. The ones that do not are the ones enterprise customers will actually ship with.
The Binary Tells Too Much
Even without running it, the shipped binary tells too much. A disassembler turns it into listings, a decompiler into source-shaped pseudocode, and the protection code itself sits in the file as a recognizable signature. Obfuscation is the counter, but obfuscation is a contradiction with a budget. It must destroy structure while preserving behavior exactly, and every transformation it applies is itself analyzable. Fixed protection code is the worst case, because both researchers and automated classifiers learn to recognize it, and once a signature exists for your protection layer, the protection layer is the first thing stripped before analysis begins.
What good obfuscation actually delivers is narrower than what most products claim. It raises the cost of analysis. It forces the analyst to spend time on transforms that would otherwise be trivial. It breaks the automation that makes mass cracking economical. It does not make the binary unreadable. It does not prevent a skilled analyst with enough time from recovering the original logic. The honest goal is making the protected binary more expensive to analyze than the value of what is inside it, and that goal is achievable with the right transforms applied correctly. The mistake most protectors make is claiming more than that, which sets expectations the technology cannot meet and erodes trust when the first public bypass drops.
AI just lowered the price of reading code
The reverse engineering toolkit has always been expensive to master. Disassembly reading, calling convention recognition, deobfuscation by hand — these were skills that took years to build and years more to apply efficiently. That learning curve was a built-in tax on anyone trying to attack protected software, and most people never paid it.
AI has largely eroded that. What used to take a skilled analyst days or weeks now takes hours. What used to take hours now takes minutes. An LLM pointed at decompiled output can identify cryptographic primitives, name obfuscated variables, reconstruct calling conventions, and explain the purpose of a function that would have taken a junior analyst a full afternoon to orient to. The barrier to entry for meaningful reverse engineering work has dropped significantly, and it has dropped for everyone regardless of their background or experience level.
The threat this creates is not that AI replaces senior analysts. Senior analysts with AI are simply faster and more productive than they were before. The real shift is at the bottom of the skill distribution. A motivated beginner with access to an LLM and a decompiler now has an orientation capability that previously required years to develop. The population of people who can meaningfully attack protected software has grown, and it will keep growing as the tooling matures. Protection schemes that relied on complexity as a passive deterrent against less skilled attackers are now facing a larger and better equipped field than they were designed for.
One product, a hundred surfaces
The code a protector must defend is never one binary. A real product ships native executables next to managed assemblies, installer DLLs next to embedded scripting engines, kernel drivers next to user-mode services, and each of them moves on its own release schedule with its own set of constraints. The native binary cannot use the same transforms as the managed assembly. The kernel driver cannot be treated the same way as a user-mode DLL. The scripting engine has a runtime the protector cannot touch. Every surface in the product is a different problem, and a protection engine that solves one of them well has not solved the others at all.
The protection itself multiplies the sprawl. Polymorphic transforms, virtualized blocks, and per-build variants mean the defender is managing a family of binaries rather than a single file, and every member of that family must be tested independently. A transform that works correctly on the main executable may corrupt a plugin that shares no code with it but loads into the same process. A virtualization scheme that passes all tests on Windows 11 may fail silently on a Windows Server 2019 build the developer never tested against. The surface area grows with every feature added to the protection engine, and the verification burden grows with it.
The challenge is not any single technique. It is the inventory. Each surface needs a protection strategy that fits its constraints, each transform needs verification that behavior was preserved, and every release cycle multiplies both numbers. A team that manually verifies each protected variant on each target surface spends more time on verification than on development. A team that skips verification ships crashes. The protectors that survive in production are the ones that treat automated correctness verification as part of the engine rather than a step the developer handles after the fact.
The arms race never pauses
Every published defense becomes a target the day it is published. The history of code protection is a history of techniques that worked until they were understood, then were automated, then were defeated at scale. Debugger detection was effective until anti-anti-debugging became a standard feature of every serious RE toolkit. Packing was effective until generic unpackers learned to snapshot memory at the OEP. Control flow flattening was effective until pattern-matching deobfuscators learned to recognize the dispatcher idiom and reconstruct the original graph. Each technique bought time. None of them bought permanence.
The ladder is visible across the last decade of tooling. Hardware breakpoints met anti-debug checks that scanned the debug registers. Those checks met emulators that spoofed the register state. Emulators met timing-based detection that measured execution speed against a known baseline. Timing detection met hypervisor-assisted debuggers that made emulation indistinguishable from native execution. Hypervisor debuggers met hardware tracing via Intel PT, which records every branch taken without touching the process at all. Counter-tracing research is already underway. Every rung of the ladder exists because the rung below it was defeated, and the pattern has not broken once in the history of the field.
Virtualization based protection was supposed to be the rung that held. Replacing native code with a proprietary bytecode running on a custom interpreter raised the cost of analysis dramatically, because the analyst had to reverse the VM architecture before reversing the protected code inside it. That advantage lasted until devirtualization tooling matured. Today, automated devirtualizers handle a significant percentage of real world VM protected samples, and the gap between a new VM design and a working devirtualizer has narrowed with every generation of tooling. The protector who ships a VM today is not shipping an unsolved problem. They are shipping a known format that the research community is actively working to break, and the research community has a strong track record.
The honest conclusion is not that protection is futile. It is that static techniques have a shelf life, and a protection engine with no update path is a protection engine with a countdown. The arms race does not pause between releases, and the only answer to a field that moves this fast is an engine that moves with it.
Why the free obfuscators are not enough
The free tools earn an honest mention here, because they are where most teams start and because dismissing them outright would be inaccurate. OLLVM and Tigress apply real transforms. They raise the floor against casual analysis and against automated scanners that expect clean compiler output. For a team with no budget and a low-value target, they are a reasonable starting point.
The problem is what they cannot do, and the list is long. Their transforms are fixed and widely studied. The same passes that ship in the public release of OLLVM today are the passes that the research community has been writing deobfuscators for since the tool was open-sourced. A pattern that appears in thousands of protected binaries is a pattern that gets a detection rule, a Ghidra plugin, and eventually a one-click removal tool. The protection literature describes this failure mode with precision. Traditional obfuscators inject recognizable protection idioms, and both targeted deobfuscation tools and machine learning classifiers are trained on exactly this class of output. A freely available pass does not stay ahead of tooling that is also freely available and actively maintained by a larger community.
They also do not verify. The open source tools transform the code and leave correctness as the developer’s problem, which is the rewriting wall from earlier in this article with no engineering on the other side of it. There is no proof that the protected binary behaves identically to the clean one. There is no test suite that runs against the transformed output. There is no diff between what went in and what came out at the semantic level. When a transform introduces a bug, the developer finds out from a customer. And when a technique breaks because a new deobfuscator handles it, nobody responds. There is no update pipeline, no patching cadence, no engineering team watching the research literature and shipping a fix before the bypass becomes a tool.
That is not a dismissal. For raising the floor against the curious and the automated, the free tools work. The gap is the ceiling, and the ceiling is engineering. A rewrite that proves its own correctness, transforms that are not publicly documented or recognizable, and a response pipeline that closes a broken technique before it becomes a commodity attack. That is the difference between a trick and a product, and it is the exact gap Obelisk was built to close
How Obelisk answers these challenges
Obelisk is the protection engine built specifically against the challenges described above. Every design decision in it traces back to one of the problems this article has named, and the sections that follow explain how each one was addressed at the engineering level rather than the marketing one.
The rewriting engine
The hardest part of protection is the part nobody sees. The transforms are visible. The rewriting engine underneath them is invisible until it breaks, and when it breaks it breaks in production, for a paying customer, in a binary they cannot debug because the protection is in the way. Every protector on the market has a rewriting engine. Most of them treat it as solved infrastructure. Obelisk treats it as the product.
The engine parses the target binary in full before touching it. Every instruction is decoded, every relocation catalogued, every exception handler walked, and every jump target recorded. The engine builds an internal representation of the binary that is complete enough to reconstruct it from scratch, because that is exactly what it does after applying transforms. The output is not a patched version of the input. It is a new binary, emitted from the internal representation, with every table rebuilt, every offset recalculated, and every section aligned correctly for the target platform. Critically, the engine does not introduce new sections. The protected binary has the same section layout as the clean one. No injected stubs, no appended segments, no new PE sections that a scanner can flag as anomalous. The transforms live inside the existing sections, which means the protected binary looks structurally normal to any tool inspecting its layout.
Signature breaking is a direct consequence of how the engine operates. Because the output binary is reconstructed from an internal representation rather than patched in place, the byte sequences that static scanners and classifiers use to fingerprint both the original code and the protection layer do not survive the rewrite. The code is functionally identical but bytewise different, and that difference is not cosmetic. It is structural, because the engine made real decisions about instruction selection, ordering, and layout during emission rather than copying the original bytes with patches applied on top.
Correctness is verified behaviorally rather than structurally. Structural verification, comparing PE headers and section layouts between the clean and protected builds, catches obvious corruption but misses semantic bugs where the binary runs but produces wrong output. Obelisk’s verification runs the protected build against the same inputs as the clean build and compares outputs. Protected builds must pass byte-identical output tests against their clean counterparts before they are considered valid. A protected binary that fails that test does not ship. The transform that caused the failure is flagged, isolated, and fixed before the next build.
The x64 exception handling problem gets its own treatment because it is the most common source of silent correctness failures in binary rewriting. Every function that can throw on x64 Windows requires an entry in the .pdata section with accurate unwind codes describing how to walk the stack frame. When Obelisk moves or extends a function, the unwind table entry for that function is rebuilt from scratch against the new layout. When a transform splits a function into multiple regions, each region gets its own unwind entry. The engine does not patch the existing table. It regenerates it, because patching a table built for a different binary layout is how subtle unwinder failures get introduced and go undetected until a customer hits an exception path that testing never covered.
Obfuscation Engine
The rewriting engine handles correctness. The obfuscation engine handles cost. Every transform it applies is designed to increase the time and skill required to recover the original logic, and the transforms are layered deliberately so that defeating one does not expose the others. The goal is not to make any single technique unbreakable. It is to make the combination expensive enough that the return on investment for the analyst goes negative before they get to anything useful.
The transforms operate at multiple levels simultaneously. At the instruction level, the engine attacks the disassembler before the analyst ever sees a listing. At the expression level, it replaces readable arithmetic and logic with equivalent forms that automated simplification cannot reduce. At the control flow level, it introduces fake structure that looks meaningful and is not, forcing the analyst to spend time proving dead paths dead rather than following live ones. The layers compound. An analyst who defeats one class of transform finds the next one waiting underneath it, and the work does not transfer.
The heaviest protection tier, dispatcher-driven control flow, is built into the engine and represents the highest cost floor of anything Obelisk applies. The details of how it operates are not published here, and that is intentional. A protection technique that is fully documented is a protection technique with a public deobfuscation target. What can be said is that it destroys the graph structure decompilers depend on to reconstruct functions, and that it does so in a way that does not introduce new sections or recognizable signatures into the output binary.
The lesson
The hardest property of this race never appears in a feature list. When a protector fails, nothing reports it. A bypassed binary runs, ships, and sells exactly like a protected one, and the vendor learns about the loss only when the cracks appear in the wild. A protector that cannot measure its own failures is racing blind, and the vendors who understand this treat protection as a process with telemetry, not a feature with a checkbox. The protectors that last are the ones that treat every published technique, including their own, as a countdown.
Research and analysis only. This article explains how code protection works and why it is hard. Nothing here promises that any protector, including Obelisk, cannot be bypassed.
