4

I'm looking into binary obfuscation for an exectuable binary written in C++. I realize preventing cracks is impossible, but making it slightly harder would be nice.

No matter how complex the actual license scheme is, I can't think of a way to validate it that doesn't ultimately boil down to:

if (doVeryComplexLicenseValidationCheck())
{
    //execute code
}

...which would be trivially easy to bypass by any competent cracker. But obviously there are many copy protection systems out there that have been very difficult for people to crack...so what are they doing that I'm missing? Is there a way to obfuscate a simple branch like that, that's much more difficult to bypass?

2
  • 1
    Did you watch the YouTube series „live overflow“? That might help. Despite the painstakingly obvious German accent, there’s parts on exactly what you are asking for C: youtu.be/qS4VWL5R_OM
    – Tobi Nary
    Sep 24, 2018 at 18:19
  • 2
    Some systems decrypt their own code on the fly using a key computed by the complicated copy protection scheme. But there are a lot of ideas and most are used in tools which are proprietary and not documented (to make it harder to crack them).
    – allo
    Sep 25, 2018 at 8:36

2 Answers 2

1

It is, in essence, an unsolvable problem.

At some point, your program needs to make a decision - is the given license key valid, or is it invalid. You cannot get around this fact.

And an attacker, who can debug your application, can look at the executed code step by step, and find the exact point where this decision is made, and change the result of it. Again, it is impossible to get around this.

Let's have a look at some historical and some modern examples:

  1. Using "fuzzy bits" during mastering

    Back when software was distributed on floppy disks, software piracy was a big problem. Users could simply copy the content of a floppy to another, and the result was pirated software. Some developers had a genius idea: A specific area on the floppy would be mastered to sit between the physical thresholds for 0 or 1. When the disk reader would read this bit, it would return different values each time it was read. Sometimes a 0, sometimes a 1.

    The idea was simply to read the value multiple times, and if it would return a different value a couple of times, then it must be a genuine floppy. This worked, because when copying a floppy, the program would read the fuzzy bit, and just write to the new floppy whatever it has read. So in the pirated copy, that bit would either be a 0 or a 1, and the software could detect that.

    Was that crackable? Of course it was. The check for that fuzzy bit was soon found by crackers and patched out, so that the function would always return that the floppy was genuine.

  2. Fake and spread license checks.

    Often times, the goal of copy protection wasn't to prevent copying forever - because even back then, developers knew it was a futile task. Instead, the goal was to delay the cracking as long as possible, so that buying a genuine copy was the only way for customers to get their hands on the program. It doesn't matter to the developer that the program is cracked a month later, because after the first month, 90% of all sales have been made already.

    One possible way to achieve this is to make cracking groups believe they already successfully cracked a program. To do that, you can place a very simple and obvious license check in the beginning, which crackers can easily patch out, and make the program look like it works. This "cracked" application is then distributed, and people begin using it - until they notice, it's not working properly.

    The trick here is that there were multiple license checks, spread out all over the program, which are triggered when certain things occur, not just when the program starts. For example, for video editing software, it can happen when the user tries to export the video, that the application places a huge watermark inside the exported video file. Or for a game, the game could decide on the second level that all enemies are now invincible. When users would then complain about this behavior, support staff would then point out to them in no uncertain terms that this is the result of them using pirated software, and that they should obtain a proper license.

    Once this behavior is well known, the cracking group will need to invest further time into cracking the software, at which point many users will already have given in and bought the software legitimately - or decided it wasn't worth bothering with.

  3. Tie in server components into your application

    This solution is the only one that "kind of" works, but depends heavily on what your application actually does. For example, many multiplayer games only allow the user to connect to a server if they have a valid license key. This means that the check whether or not a given license key is valid happens on hardware that the user does not control, and thus cannot circumvent.

    Of course, the viability of this approach depends on your application. While this may work well for online games, it does absolutely nothing to stop an attacker from pirating something like a video editing software.

  4. Give up

    Some portion of your users will pirate your software. This is just an inevitable fact. So don't waste your resources trying to prevent them from doing so, when you could stick those resources into the next big release of your software, which you can then sell to your honest customers.

    If you want, you can add a message to your software, which thanks the user for being honest, and reminding them that their honesty is the reason your company still exists and is able to provide them with the software which they hopefully find useful. It may be enough to convince some people that spending 14.99 USD on a software they use every other day could really be worth it.

1
  • 1
    Another technique you can use is to use license checks in computations rather than hardcoding constants. Of course it's very possible to bypass, but it's not as easy as modifying a simple conditional jump.
    – forest
    Jul 5, 2021 at 3:10
-3

Here are some tips that may help you, but just remember that any one with a good understanding of assembler will found it.

Lets imagine your program is like you describe:

int main() {

    if (myCheck()) {
            std::cout << "License pass\n";
    }
    ....

And here is the disassembler of the code.

00000000004006d3 <main>:
4006d3:       55                      push   %rbp
4006d4:       48 89 e5                mov    %rsp,%rbp
4006d7:       e8 da ff ff ff          callq  4006b6 <_Z7myCheckv>
4006dc:       84 c0                   test   %al,%al
4006de:       74 0f                   je     4006ef <main+0x1c>
4006e0:       be d1 07 40 00          mov    $0x4007d1,%esi
4006e5:       bf 40 10 60 00          mov    $0x601040,%edi
4006ea:       e8 b1 fe ff ff          callq  4005a0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
4006ef:       b8 00 00 00 00          mov    $0x0,%eax
4006f4:       5d                      pop    %rbp
4006f5:       c3                      retq

So by adding asm statements in your code you can make it more difficult.

#define OPS1 "xor \%rbx,\%rbx\n"
int main() {
    __asm__ volatile (
            "push %rax\n"
            "xor %rax,%rax\n"
            "pop %rax\n"
    );

    if (myCheck()) {
            std::cout << "License pass\n";
    }
    __asm__ volatile (OPS1);

And here is the output

4006d3:       55                      push   %rbp
4006d4:       48 89 e5                mov    %rsp,%rbp
4006d7:       50                      push   %rax
4006d8:       48 31 c0                xor    %rax,%rax
4006db:       58                      pop    %rax
4006dc:       e8 d5 ff ff ff          callq  4006b6 <_Z7myCheckv>
4006e1:       84 c0                   test   %al,%al
4006e3:       74 0f                   je     4006f4 <main+0x21>
4006e5:       be e1 07 40 00          mov    $0x4007e1,%esi
4006ea:       bf 40 10 60 00          mov    $0x601040,%edi
4006ef:       e8 ac fe ff ff          callq  4005a0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
4006f4:       48 31 db                xor    %rbx,%rbx
4006f7:       b8 00 00 00 00          mov    $0x0,%eax
4006fc:       5d                      pop    %rbp
4006fd:       c3                      retq

This case is really really easy, but if you have a good understanding of templates, assembler you complicate a bit the code generated.

2
  • I fail to see how that would "make it more difficult". Inserting random instruction sequences doesn't change the fact that there's a single license-checking function, and it may interfere with the normal operations of the compiler.
    – user82155
    Oct 24, 2018 at 22:29
  • This is just an example, you can put the random instructions inside the function on whatever place you want, of course any one with some experience on assembler will get the result but depending on the code that you put this can be a hard work.
    – camp0
    Oct 25, 2018 at 14:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.