I sell a commercial MT5 trading bot (MT5 = MetaTrader 5, the retail forex/CFD terminal — if you’ve never touched it, think “a niche ecosystem of paid expert-assistant programs that run inside a trading platform”, with its own file format, .ex5, and its own marketplace culture). One afternoon a buyer sent me a screenshot: my bot — the one I price at $1,500 — listed on a Telegram channel for $10, inside a “group buy” bundle of forty-plus bots. Crypto payments, instant delivery, a support channel of its own. The pirate operation looked more professional than my sales page.
The uncomfortable part was not the piracy itself. I had made peace with the idea that piracy exists. The uncomfortable part was how it worked. My bot had not been decompiled. It had not been cracked in any sense I understood the word at the time. Someone had bought one legitimate license, run the bot once, and read it out of the memory of a live process. Total effort, as far as I could reconstruct later: under two minutes.
This post is the full write-up of that rabbit hole: how the attack actually works, why every defense I had in place — and every defense most bot vendors have in place — is irrelevant to it, what I ended up rebuilding, and the measurements I took along the way. It’s written from the perspective of a bot vendor who got burned, not a security company selling you something. That perspective turned out to matter: almost all the standard advice comes from people who have never had to live with the consequences of it failing.
Before the technical part, it’s worth understanding the economics, because the economics dictate what a defense must accomplish.
Trading bots are a peculiar product:
Group-buy channels aggregate exactly that demand: forty people chip in $10 each, one of them buys (or cracks) a single legit copy, everyone gets the bot. The channel operator takes a cut of every bundle. This is not one bored teenager with IDA Pro; it’s a distribution business with customers, support and repeat purchase behavior.
The consequence for defenders is brutal and clarifying: you do not need to stop a motivated reverse engineer. If your security posture assumes a nation-state adversary, you will spend unlimited effort and still lose. What you need is to make the cost of producing one working resale copy exceed the revenue a $10 copy generates. Break the unit economics of the group-buy bundle and the industry moves on to the next bot whose economics are still intact.
Keep that framing in mind for everything below. “Unhackable” is not a goal, not a claim, and not what I built.
Who attacks a commercial MT5 bot, and what can they actually do?
What they can all do: run the bot with a valid license on a machine they fully control. That last clause is the whole problem statement. Any protection scheme whose guarantees depend on the client machine being friendly is protecting nothing, because the pirate’s machine is maximally unfriendly by construction.
What we are not defending against, up front:
I had most of this list in place when my bot got resold. Here is the honest post-mortem of each.
The classic advice for .ex5 distribution: wrap the file, encrypt the payload, unpack at load time. The failure mode is structural. A wrapper is a self-decrypting archive. Whatever key unlocks the payload has to be available to the unpacker on the client machine — so the key is on the machine, and the plaintext is in memory one instruction after unpacking. The wrapper protects the file on disk, and the file on disk was never the thing being stolen.
There’s a subtler point that took me a while to internalize: a packed binary makes the static analysis harder and the runtime capture identical. You have relocated the secret from “find it in the file” to “find it in memory”, and memory is exactly where the attacker is looking. You’ve done the pirate a favor by consolidating everything into one decrypt-at-once event.
String encryption, control-flow flattening, opaque predicates, symbol stripping. I shipped a build with all of it. The post-mortem:
Both are good ideas that fail alone. A HWID lock is a string comparison inside the process; patch it out, or read a valid HWID response from a proxy. An online check phones home once and then caches its result in — you guessed it — process memory, next to everything else worth stealing. The check itself becomes another artifact in the dump.
Self-hash checks fail to the same runtime argument: whatever they verify, the verified result sits in memory next to everything else. Anti-debug is an arms race the defender loses by default at user level — public tooling iterates faster than vendor checks — and it has an ugly side effect: aggressive anti-debug behavior is exactly the behavioral profile that makes antivirus engines flag your binary, which produces a failure mode worse than piracy (below).
One thing I got wrong for months: my early hardening attempts made the binary look like malware to heuristic antivirus (AV) engines. When your customers’ terminals start quarantine-popping your product, you get chargebacks instead of piracy. Any defense in this space must end a scans clean across all major engines — not as a nice-to-have, as a release gate. I’ll return to the numbers in section 8.
A running process leaks its memory from several directions, all public, all unremarkable:
None of these require exotic privilege, and — the part that matters — none of them are detectable-and-blockable from inside the target process in any reliable way. The read happens outside your code’s control flow. This is the asymmetry that kills client-side trust models: you cannot prevent the read, so the only remaining move is to make what gets read worthless.
A dump of a running, licensed trading bot is embarrassingly rich:
A pirate does not need to “understand” any of this. Pattern-matching is enough, and the patterns are strong.
This is where the attack goes from “possible” to “minutes”. The standard repertoire:
The result of this stage is not understanding — it’s a shopping list of memory regions, each with a known meaning.
With the shopping list, the pirate has options in ascending effort: repackage the extracted weights/logic into their own harness; patch a copy of the binary to skip licensing entirely (now that the dump shows exactly which checks exist); or replay/clone whatever the licensing accepts. The first option is the bundle operator’s favorite because it needs zero understanding of the vendor’s code — and because the extracted assets are exactly the parts customers pay for.
Run the numbers on the full pipeline: one legit license (~$1,500, or less on promotion), minutes of tool time, and a distribution channel that already exists. Against that: forty sales at $10 each, minus channel fees. When I did this arithmetic about my own bot, the conclusion was unavoidable — at the prevailing level of protection, pirating my product was a rational business. No amount of obfuscation changes that; only changing what a dump yields does.
Given the asymmetry in 5.1 — you cannot prevent the read, only change what a read yields — the design constraints write themselves:
The core mechanism. All sensitive material lives in a dedicated memory region — the arena — under a regime where no single address ever exposes usable plaintext:
A diagram of the difference, because it’s the whole post in one picture:
Naive: Masked arena:
heap @ 0x7F3A00 arena fragment layout (differs per run)
[ 2048-byte key, plaintext ] 0x...40: 9A F2 11 .. (masked frag)
[ weights array, plaintext ] 0x...71: 04 5C 8D .. (unrelated frag)
[ strings, plaintext ] 0x...B3: 77 10 E2 .. (mask state)
...no contiguous plaintext anywhere
In the left picture, one dump is the whole theft. In the right picture, a dump yields fragments whose meaning and assembly change between runs. The attacker’s reassembly work goes from “read the shopping list” to “reverse-engineer the mask protocol” — and the mask protocol is code, which changes with every build, which is what finally puts the curious expert back in the cost equation too.
Static keys are standing targets: copy once, valid forever. The licensing model moved to one-time tokens, each an Ed25519-signed grant valid for exactly one session:
This also cleans up the licensing business logic that used to live in the dump: there is no long-lived key to find, so the dump no longer contains one.
The piece I initially resisted building, and now consider the centerpiece. When a license is revoked, an operator-signed kill verdict (0xFF) reaches the session and the arena burns: masks are discarded, fragments are zeroized, the region is overwritten — in about one second, end to end.
The design test for revocation is not “can I turn a license off”. It is: after the off-switch, what does a dump of the process return? If the answer is “the secrets, still there in some form”, the revocation is decorative. After a burn, the answer is zeros — the resale copy that a bundle operator was assembling dies mid-assembly, and the $10 listing becomes a refund queue.
Fail-closed matters doubly because revocation is the one lever you pull after abuse is detected. Detection will always be reactive — someone reports the Telegram listing, you check the token ledger, you kill the session. The entire defense end-to-end is: tokens leak at most one session each, and each leaked session burns inside a second of being noticed.
The hardened build must present the same profile as an ordinary native binary: no packer signatures, no section anomalies, no behavioral tells. Hence the release gate — every build is scanned, and 0 detections across all 60 engines is a ship/no-ship criterion, not a marketing stat. (The full numbers are in the next section.)
A design that can’t be adopted doesn’t protect anyone, so the boundary matters as much as the mechanism. The engine exposes bindings for MQL5, C# and Python, with an MQL5 Integration Kit that covers the .ex5 workflow end to end. The practical shape: the bot’s secret-bearing parts are delegated to the protected engine; the MQL5 side orchestrates and trades; the arena, tokens and kill switch live below the .ex5 boundary where the dump attacker lives.
The vendor-facing consequence is the interesting part: the visible binary shrinks to a stub. What a reverse engineer sees of a protected build — symbols, strings, size — collapses, because the substance isn’t in the file anymore at all.
Numbers from the final build. Methodology note: “before” is the shipping v1 binary with wrapper + obfuscation (the standard stack); “after” is the protected build. “What a reverse engineer gets” means: material recoverable from a full memory dump plus static analysis of the shipped file.
| What a reverse engineer gets | Before | After |
|---|---|---|
| Visible binary size | 16.0 MB | ~1.3 MB |
| Symbols | 49,237 | 0 |
| Readable strings | many | 0 |
| Contiguous key/weights in dump | present | absent |
| Token replay value | permanent | one session |
| Revocation burn time | — | ~1 s |
| License handshake | — | ~0.01 s |
| VirusTotal detections | — | 0/60 |
Two readings of this table:
Stated plainly, because the credibility of every claim above depends on the honesty of this section:
Things I have not settled, offered to anyone working on the same problem:
I ended up building the above into a small protection service for other bot authors — that’s context, not the point of this write-up, and this is the only sentence in which it appears. If you work on memory protection or licensing design and can poke a hole in the threat model, I want to hear it. Section 10 is an honest list of where I think the remaining holes are.