Glengamoi (Forum) · AspHeute · .NET Heute (RSS-Suche) · AspxFiles (Wiki) · .NET Blogs

Kopieren verboten - Lizenzsicherung bei ASP Scripts

Geschrieben von: Claudius Ceteras
Kategorie: ASP Tricks

This printed page brought to you by AlphaSierraPapa

Textdateien im allgemeinen und ASP Skripte im speziellen lassen sich nicht wie konventionelle Software sichern. Dieser Artikel soll einen Weg aufzeigen, wie man, wenn auch nicht das eigentliche Kopieren verhindern, so doch Lizenzverstöße nachvollziehen kann.

Die Beispiels-Anwendung, die für diesen Artikel implementiert wurde, nutzt ActiveFile von Infomentum um ZIP-Archive zu erzeugen, sowie für den Datei-Download. Die Installation sowie grundsätzliche Verwendung dieser Komponente ist aber nicht primäres Thema dieses Artikels und kann im Artikel Zippen und entzippen von Dateien nachgelesen werden.

Problemstellung

Eine beliebte Möglichkeit ASP Skripts zu schützen ist es, diese in COM Komponenten zu kapseln, wobei man dann die Möglichkeit hat Funktionalitäten z.B. erst durch Eingabe eines Lizenzcodes freizuschalten. Diese Lizenzcodes sind meist aus dem Namen des Käufers berechnet und nur damit verwendbar, was effektiv das Weitergeben der Software inklusive Freischaltcode verhindert - jedenfalls solange der Berechnungs-Algorithmus nicht geknackt wird. Solche Möglichkeiten hat man mit Skripten nicht, denn jeglicher Schutzmechanismus wäre ohne Probleme aus dem unkompilierten Code zu entfernen.

Warum dann überhaupt ASP-Funktionalität in Form von Skript anbieten und nicht immer als Komponente? Ein möglicher Grund ist z.B. die größere Käufergruppe, die man mit Skripten erreicht, denn nicht jeder hat die Möglichkeit Komponenten auf den verwendeten Servern zu registrieren.

Also, wie schütze ich dann meine Software, die ich in Form von Skripten verkaufen möchte?

Lösungsansatz

Die verkaufte Kopie einer Software muß wiedererkennbar werden. Dazu wird eine Seriennummer in die ASP Skripte eingebaut. Diese Seriennummer sollte folgende Eigenschaften haben:

  1. Von außen - also als Surfer - lesbar sein, damit man leicht Lizenzverstöße nachweisen kann.
  2. Eindeutig und nicht zu erraten sein, damit nicht beim Weitergeben die Nummer eines anderen Kunden eingetragen werden kann.
  3. Nicht als Seriennummer erkennbar sein, damit sie nicht entfernt wird.

Die erste Eigenschaft ist dadurch zu erreichen, daß man die Nummer nicht als VBS-Kommentar innerhalb des ASP-Codes, sondern z.B. als HTML-Kommentar, als Kommentar in clientseitigem Skript-Code oder in CSS-Dateien einträgt.

Die zweite Eigenschaft erreicht man dadurch, daß die Seriennummer lang genug ist und auf keinen Fall fortlaufend sein darf. Man sollte hier also eher zB eine GUID wählen.

Für die letzte Eigenschaft muß man die Seriennummer verstecken und tarnen. Dazu kann man die Seriennummer auch auseinanderschneiden und auf mehrere Stellen innerhalb einer Datei oder sogar auf mehrere Dateien verteilen. Tarnmöglichkeiten gibt es viele, z.B. als Versionsnummer:

<!-- My ASP Application - Version 2.73.1254 -->

Oder als Farben in ungenutzten Klassen innerhalb von CSS-Dateien:

td.data1 { color: #12aabb; background-color: #3a5cd8}

Natürlich gibt es hier noch viel mehr Möglichkeiten, und man kann sich selbst ein geeignetes Versteck für die Seriennummer ausdenken.

Implementierung

Basierend auf obiger Spezifikation soll ein Prototyp eines Systems entstehen, mit dem man registrierten Kunden per Seriennummer personalisierte Software-Pakete zum Download anbieten kann. Für das on-the-fly zippen und den Download wird ActiveFile von Infomentum verwendet, wobei man natürlich beliebig andere ZIP- bzw. Download-Komponenten nutzen kann. Die Seriennummer-Funktionalität wird in einer VBS-Klasse gekapselt, um sie auch außerhalb dieses Prototyps wiederverwenden zu können.

SoftwarePackagePersonalizer

Die zu erzeugende Klasse soll aus einem Quell-Verzeichnis einige Dateien (CopyFiles-Array) nur in ein Ziel-Verzeichnis kopieren und andere (ParseFiles-Array) Dateien beim Kopieren nach bestimmte Formulierungen suchen und diese durch Seriennummern ersetzen. Dies führt zu folgendem Klassen-Design:

class SoftwarePackagePersonalizer
    property let/get SourceDirectory
    property let/get DestinationDirectory
    property let/get CopyFiles
    property let/get ParseFiles
    function Go()
end class

Die Function Go() führt den Kopierprozess durch und gibt dann die komplette Seriennummer zurück. Beim Parsen der Dateien soll nach folgendenden Formulierungen gesucht werden, welche dann durch Seriennummernfragmenten ersetzt werden:

@@number:type:length@@

number gibt die Position des Seriennummernfragments innerhalb der Seriennummer an, type bestimmt den Typ des zu erzeugenden Fragments, wobei hier folgende Typen implementiert sind:

Typ Bedeutung
n Numerisch: Dieser Typ erzeugt Fragmente aus den Zeichen 0-9
a Alpha: Dieser Typ erzeugt Fragmente aus den Zeichen a-z
an Alphanumerisch: Dieser Typ erzeugt Fragmente aus den Zeichen 0-9 und a-z
h Hex: Dieser Typ erzeugt Fragmente aus den Zeichen 0-9 und a-f

Auch andere Typen sind denkbar, z.B. welche, die ein Zufallsdatum erzeugen etc. length letztendlich bestimmt die Länge des zu erzeugenden Fragments.

Zum leichteren Verständnis hier einige Beispiele:

Code Erzeugtes Beispiels-Fragment Beschreibung
@@1:h:6@@ 12a4f6 Gut um Seriennummern als Farbe in CSS-Dateien zu tarnen
@@2:n:4@@ 3968 Möglicher Einsatz: Erzeugung von Versionsnummern

Vorgenanntes Beispiel würde dann zur Seriennummer 12a4f63968; die Seriennummerfragmente werden also in der richtigen Reihenfolge zur Seriennummer zusammengesetzt.

Wie sieht das ganze als Code aus?

Alles außer der Go-Funkion ist trivial. Das einzig bemerkenswerte ist, dass darauf geachtet wird, daß der Verzeichnispfad mit einem "\" abschließt.

<%
class SoftwarePackagePersonalizer
    private m_SrcDir
    private m_DestDir
    private m_CopyFiles
    private m_ParseFiles

    private sub Class_Initialize()
      m_CopyFiles = Array()
      m_ParseFiles = Array()
    end sub

    property let SourceDirectory(value)
      m_SrcDir = value
      if right(m_SrcDir,1)<>"\" then
        m_SrcDir = m_SrcDir & "\"
      end if 
    end property
    
    property get SourceDirectory
      SourceDirectory = m_SrcDir
    end property

    property let DestinationDirectory(value)
      m_DestDir = value
      if right(m_DestDir,1)<>"\" then
        m_DestDir = m_DestDir & "\"
      end if 
    end property

    property get DestinationDirectory
      DestinationDirectory = m_DestDir
    end property

    property let CopyFiles(value)
      m_CopyFiles = value
    end property
    
    property get CopyFiles
      CopyFiles = m_CopyFiles
    end property

    property let ParseFiles(value)
      m_ParseFiles = value
    end property
    
    property get ParseFiles
      ParseFiles = m_ParseFiles
    end property

    function Go()
      dim fso, stream, txt, regEx, matches, match
      dim num, typ, leng, randStr, serials, dummy
      dim i, j, result, filename
    
      dim forReading : forReading = 1
      dim forWriting : forWriting = 2

      Set fso = Server.CreateObject("Scripting.FileSystemObject")

Nach dem Erstellen des FileSystemObjects werden zuerst die Dateien kopiert, die nicht verändert werden müssen:

      ' 1 Copy Files
      for each filename in m_CopyFiles
        fso.CopyFile m_SrcDir & filename, m_DestDir, true
      next

Danach werden die zu parsenden Dateien einzeln eingelesen und nach dem erwähnten Muster durchsucht:

      ' 2 Parse Files
      for each filename in m_ParseFiles
          ' 2.1 read file content
          Set stream = fso.OpenTextFile(m_SrcDir & filename,forReading)
          txt = stream.readAll()
          stream.close
          ' 2.2 search for @@number:type:length@@ …
          Set regEx = new RegExp
          regEx.Global = true
          regEx.Pattern = "@@([^:]*):([^:]*):([^@]*)@@"
          Set matches = regEx.Execute(txt)

Die gefundenen Muster werden in ihre Bestandteile zerlegt und an die Funktion RandomString übergeben, die wir später noch sehen werden (diese kann uns die verschiedenen Typen der Seriennummernfragmente erzeugen). Das erzeugte Fragment wird an die Stelle des gefundenen Musters im eingelesenen Text gesetzt und zusätzlich zusammen mit der Position in der Seriennummer in der Variable serials gespeichert. Damit können wir später daraus die Seriennummer zusammensetzen.

Zum Speichern der Seriennummerfragmente wird ein CSV-Format benutzt.

          for each match in matches
            num = CInt(match.subMatches(0))
            typ = match.subMatches(1)
            leng = CInt(match.subMatches(2))
            randStr = randomString(typ,leng)
            ' 2.3 replace token with random String
            txt = replace(txt,match.Value,randStr)
            serials = serials & vbCr & num & vbTab & randStr
          next

Jetzt kann man den geänderten Dateininhalt in die Zieldatei schreiben und das gesamte Software-Paket ist personalisiert.

          Set stream = fso.OpenTextFile(m_DestDir & filename,forWriting,true)
          ' 2.4 ... and write the altered content into the created file
          stream.write txt
          stream.close
      next
      Set fso = Nothing

Das einzige, was jetzt noch zu tun bleibt, ist die Seriennummern aus den Fragmenten zusammenzusetzen und zurückzugeben. Hierzu wird das CSV-Format in einem Array von Arrays expandiert, die einzelnen Fragmente mit einem Bubble-Sort-Derivat nach der Position sortiert und schließlich zur fertigen Seriennummer zusammengefügt.

      ' 3 return serial
      serials = mid(serials,2) ' delete first vbCr
      ' 3.1 unpack serial parts
      serials = split(serials, vbCr)
      for i = 0 to ubound(serials)
         serials(i) = split(serials(i), vbTab)
      next
      ' 3.2 bubble-sort serial parts
      for i = 0 to ubound(serials)-1
         for j = ubound(serials) to i+1 step -1
            if serials(i)(0)>serials(j)(0) then
               dummy = serials(j)
               serials(j) = serials(i)
               serials(i) = dummy
            end if
         next
      next
      ' 3.3 compile serial
      result = ""
      for i = 0 to ubound(serials)
         result = result & serials(i)(1)
      next
      Go = result
    end function

Anwendungsbeispiel

Die eben erstellte Klasse soll eingesetzt werden um personalisierte gezippte Software-Pakete anzubieten. In diesem Fall ist es eine Beispiels-ASP-Applikation, die zwei Zahlen addieren kann.

Als erstes brauchen wir ein Registrierungs-Formular. Zur Demonstration reicht die einfache Abfrage nach Namen und Email-Adresse. Auch verzichten wir hier auf besondere Überprüfungen der Eingaben - Hilfe hierzu bekommt man unter anderem im Artikel Überprüfen von HTML-Formularen mit ASP und den weiteren Links dazu.

<html>
<body>
  Zum Download des SuperDuper-ASP-Addierers musst Du Dich hier registrieren:
  <form action="download.asp" method="post">
    name: <input name="name"><br>
    email: <input name="email"><br>
    <input type="submit" value="abschicken">
  </form>
</body>
</html>

In download.asp wird zuerst ein temporäres Verzeichnis erstellt, damit sich die einzelnen Downloads des Pakets nicht gegenseitig stören:

<%option explicit%>
<!--#include file="SoftwarePackagePersonalizer.asp" -->
<%
  dim fso, tmp, serial
  Set fso = CreateObject("Scripting.FileSystemObject")
  ' generate temp folder...
  tmp = Server.MapPath(fso.GetTempName)
  fso.CreateFolder tmp

Dann wird die erstellte Klasse benutzt, um das personalisierte Paket in diesem Verzeichnis zu erstellen. Das Paket besteht aus der Datei readme.txt, die nur kopiert wird und default.asp und style.css, die insgesamt drei Seriennummernfragmente enthalten (vollständiger Code im heutigen Download enthalten).

  ' ... and create the software package there.
  dim spp
  Set spp = new SoftwarePackagePersonalizer
  spp.SourceDirectory = Server.MapPath("calculator-package")
  spp.DestinationDirectory = tmp
  spp.CopyFiles = Array("readme.txt")
  spp.ParseFiles = Array("default.asp","style.css")
  serial = spp.Go()

Nun wird noch alles mit den ActiveFile-Komponenten gezippt und zum Client geschickt:

  ' now put everything in a zipfile, ...
  dim Archive
  Set Archive = Server.CreateObject("ActiveFile.Archive")
  Archive.NewArchive tmp & "\calculator.zip"
  Archive.Add tmp & "\*.*"
  Archive.SaveArchive
  Set Archive = Nothing

  ' ... download the ZIP archive ...
  dim File
  Set File = Server.CreateObject("ActiveFile.File")
  File.Name = tmp & "\calculator.zip"
  Response.Clear
  Response.AddHeader "Content-Disposition", "inline; filename=calculator.zip"
  File.Download "application/x-zip-compressed", Now()
  Set File = Nothing

Zum Schluß löscht man nur noch das temporäre Verzeichnis mit allen Dateien darin und merkt sich die erstellte Seriennummer zusammen mit den Registrierungsdaten. In diesem Beispiel speichern wir alles der Einfachheit halber nur in einer Textdatei. Im Produktions-Einsatz nimmt man hierzu natürlich eine Datenbank.

  ' and delete everything. (comment this to see the temp folder and its content)
  fso.DeleteFolder tmp, true
    
  ' write serial and name to a log-file.
  ' this would be a database on a production site of course...
  const ForAppending = 8
  dim stream
  Set stream = fso.OpenTextFile(Server.MapPath(".") & _
    "\log.txt", ForAppending, true)
  stream.writeLine "----------------------------"
  stream.writeLine "Name: " & Request("name") & _
    " (" & Request("email") & ")"
  stream.writeLine "Serial: " & serial
  stream.close
  Set stream = Nothing
    
  Set fso = Nothing
%>

Entdeckt man nun irgendwo seine Software, braucht man sich nur noch aus den entsprechenden Dateien die Seriennummerfragmente zusammenzusuchen, und sie dann mit den Einträgen in der eigenen Datenbank/Logdatei zu vergleichen, um herauszufinden an wen das Paket lizensiert wurde. Natürlich kann man hierzu auch eine Web-Applikation erstellen, die einem diese Vergleichsarbeit abnimmt, vielleicht sogar teilweise manipulierte Seriennummern noch erkennt. Eine weitere nützliche Erweiterungen wäre es bei Eingabe einer URL die Dateien mit den Seriennummerfragmenten herunterzuladen ( z.B. mit ASPTear oder XMLHTTP) und die Seriennummer automatisch zu extrahieren.

Schlußbemerkung

Wir haben gesehen, wie man mit einfachen Mitteln durch Seriennummern abgesicherte Pakete zum Download anbieten kann. Eine Beschränkung dieser Technologie ist es natürlich, daß die Seriennummer nicht funktionaler Bestandteil des Skripts ist, also bei Entdeckung entfernt werden könnte. Es kommt also darauf an, die Seriennummer gut zu verstecken.

This printed page brought to you by AlphaSierraPapa

Download des Codes

Klicken Sie hier, um den Download zu starten.
http://www.aspheute.com/code/20020411.zip

Verwandte Artikel

Überprüfen von HTML-Formularen mit ASP
http:/www.aspheute.com/artikel/20000522.htm
ASP Scripts verschlüsseln
http:/www.aspheute.com/artikel/20000510.htm
Klassen in VBScript
http:/www.aspheute.com/artikel/20000526.htm
Laden von Dateien aus dem Web mit ASP
http:/www.aspheute.com/artikel/20000519.htm
MS Script Encoder dekodiert
http:/www.aspheute.com/artikel/20011123.htm
Sonderzeichen korrekt grabben mit XmlHttp
http:/www.aspheute.com/artikel/20011113.htm
Webpage-Grabbing mit dem XML Parser
http:/www.aspheute.com/artikel/20010328.htm
Webseiten automatisiert scrapen
http:/www.aspheute.com/artikel/20010910.htm
Webseiten automatisiert scrapen, Teil 2
http:/www.aspheute.com/artikel/20010911.htm
Zippen und entzippen von Dateien
http:/www.aspheute.com/artikel/20001005.htm

Links zu anderen Sites

Infomentum ActiveFile
http://www.infomentum.com/activefile/

 

©2000-2006 AspHeute.com
Alle Rechte vorbehalten. Der Inhalt dieser Seiten ist urheberrechtlich geschützt.
Eine Übernahme von Texten (auch nur auszugsweise) oder Graphiken bedarf unserer schriftlichen Zustimmung.