Lizenzprüfungen umgehen

Aus Gründen(TM) musste ich mir über Lizenzmanagement bei kommerzieller Software Gedanken machen. Wenn man einen Lizenzmanagement-Mechanismus entwickelt setzt man üblicherweise auf asymmetrische Signaturen, sprich: Ähnlich wie bei einer Email-Signatur kommt ein Kryptoverfahren zum Einsatz, das den privaten Schlüssel eines Lizenzgebers nutzt, um eine Lizenz “zu unterschreiben”. Das ist auch die richtige Idee und funktioniert ganz gut.

Allerdings wird oft vergessen, das man das ausgelieferte Software-Binary ja verändern kann. Ein Experiment in C:

#include <stdio.h>

enum License {
    VALID = 1,
    INVALID = 2,
};

int checklicense(void) {
    return INVALID;
}

int main (int argc, char const* argv[]) {
    if (checklicense() == VALID) {
        printf("License is valid\n");
    } else {
        printf("License is invalid\n");
    }
    return 0;
}

Das ist natürlich stark vereinfacht, aber es geht ja darum, meinen Punkt zu veranschaulichen. Aus diesem Quellcode ist via

$ gcc -o main -O0 main.c

schnell ein Binary erzeugt. Via -O0 schalte ich die Optimierungen des Compilers aus, damit das Kompilat simpel bleibt. Beim Starten gibt das Programm

$ ./main
License is invalid

aus, denn die Funktion checklicense() gibt ja den Wert INVALID zurück. In einem realen Programm wäre dort die Lizenzprüfung untergebracht — hier ist alles sehr vereinfacht.

So weit so gut; bis jemand ein Reverse Engineering-Werkzeug wie Radare2 öffnet. Diese Werkzeuge sind ziemlich gewöhnungsbedürftig, aber sehr mächtig. Ich öffne das Binary also gleich mal im Schreibmodus (-w), damit ich einzelne Bytes direkt in r2 patchen kann:

$ r2 -Aw main

-A analysiert das Binary direkt beim Öffnen, alternativ hätte ich auch den Befehl aaaa in der Konsole nutzen können. Da die Analyse nun aber schon durch ist kann ich mittels

s main
pdf

direkt zur main-Funktion springen und mir den aktuellen Frame als Assembler-Code anzeigen lassen:

[0x00400430]> s main
[0x00400531]> pdf
|           ; CODE (CALL) XREF from 0x00400531 (unk)
/ (fcn) sym.main 54
|           0x00400531    55           push rbp
|           0x00400532    4889e5       mov rbp, rsp
|           0x00400535    4883ec10     sub rsp, 0x10
|           0x00400539    897dfc       mov [rbp-0x4], edi
|           0x0040053c    488975f0     mov [rbp-0x10], rsi
|           0x00400540    e8e1ffffff   call sym.checklicense
|              sym.checklicense(unk)
|           0x00400545    83f801       cmp eax, 0x1
|       ,=< 0x00400548    750c         jnz 0x400556
|       |   0x0040054a    bff4054000   mov edi, str.Licenseisvalid
|       |   0x0040054f    e8acfeffff   call sym.imp.puts
|       |      sym.imp.puts()
|      ,==< 0x00400554    eb0a         jmp loc.00400560
|      |`-> 0x00400556    bf05064000   mov edi, str.Licenseisinvalid
|      |    ; CODE (CALL) XREF from 0x00400400 (fcn.004003fc)
|      |    0x0040055b    e8a0feffff   call sym.imp.puts
|      |       sym.imp.puts()
|      |    ; CODE (CALL) XREF from 0x00400554 (unk)
|- loc.00400560 7
|      `--> 0x00400560    b800000000   mov eax, 0x0
|           0x00400565    c9           leave
\           0x00400566    c3           ret

Nu ist eigentlich recht schnell zu sehen, das nach dem call sym.checklicense der Rückgabewert der Funktion im eax-Register liegt. Die Anweisung

cmp eax, 0x1

vergleicht diesen Rückgabewert mit VALID, also dem Wert 0x1. Ich kann nun direkt an diesen Offset im Binary springen und den Wert gegen 0x2 austauschen:

[0x00400531]> s 0x00400545
[0x00400545]> wx 83f802
[0x00400545]> pdf
|           ; CODE (CALL) XREF from 0x00400531 (unk)
/ (fcn) sym.main 54
|           0x00400531    55           push rbp
|           0x00400532    4889e5       mov rbp, rsp
|           0x00400535    4883ec10     sub rsp, 0x10
|           0x00400539    897dfc       mov [rbp-0x4], edi
|           0x0040053c    488975f0     mov [rbp-0x10], rsi
|           0x00400540    e8e1ffffff   call sym.checklicense
|              sym.checklicense(unk)
|           0x00400545    83f802       cmp eax, 0x2
|       ,=< 0x00400548    750c         jnz 0x400556
|       |   0x0040054a    bff4054000   mov edi, str.Licenseisvalid
|       |   0x0040054f    e8acfeffff   call sym.imp.puts
|       |      sym.imp.puts()
|      ,==< 0x00400554    eb0a         jmp loc.00400560
|      |`-> 0x00400556    bf05064000   mov edi, str.Licenseisinvalid
|      |    ; CODE (CALL) XREF from 0x00400400 (fcn.004003fc)
|      |    0x0040055b    e8a0feffff   call sym.imp.puts
|      |       sym.imp.puts()
|      |    ; CODE (CALL) XREF from 0x00400554 (unk)
|- loc.00400560 7
|      `--> 0x00400560    b800000000   mov eax, 0x0
|           0x00400565    c9           leave
\           0x00400566    c3           ret
^D

Das Binary prüft nun also, ob die Lizenz ungültig ist, und springt dann in den Pfad für eine erfolgreiche Lizenzprüfung:

$  ./main 
License is valid

Tjoa. Und schon ist die schöne Lizenzprüfung umgangen. Es kommt also nicht nur darauf an, das Verfahren zur Erzeugung einer Lizenzdatei gut zu durchdenken. Sondern auch die Prüfung auf Gültigkeit ist gar nicht so einfach, jedenfalls nicht, wenn man derartige Angriffe ausschließen will. Es hat schon einen Grund, warum Firmen wie WIBU Systems sich auf genau diese Problemstellungen spezialisiert haben.