Coding for Dollars: Copy Protection and Prevention

From EDM2
Jump to: navigation, search

Written by Larry Salomon Jr.

Introduction

Have you ever written a wonderful application, decided to distribute it commercially, get little money for it, but find out that many people have a copy nonetheless? Ask Gordon Zeglinski; or ask Describe; or ask any of the many commercial developers in the world what it feels like to see your revenue lost due to piracy.

I am reminded specifically the story of the brother of an ex-girlfriend of mine: he wrote a really good cribbage program for DOS but received only approximately 40 orders for the program. He lived in Raleigh, where the National Cribbage Tournament is held annually and everyone at the next tournament told him how good his program was. He was ready, though; he had memorized the names of all of the people that had ordered the program and confronted those that had not on the spot.

Of course, this isn't considered the best way to handle this situation, especially since he didn't receive any more money from the people who he had "caught with their hands in the cookie jar." No, the best way is to follow the age-old adage, "an ounce of prevention is worth a pound of cure." This article will attempt to do just that - develop code that will enable an application to keep track of the number of licenses available and prevent unauthorized copying of the application.

The reader should be forewarned: if a software pirate wants something bad enough, they will get it. I can remember the days when I bought the COPYIIPC hardware adapter which copied the analog representation of a diskette's data to another. This hardware-assisted "archival copy enabler" ("Yeah, that's the ticket...") could copy anything, including software that used the infamous weak-bit protection scheme. The point is that no protection scheme is 100% secure, much less this one which I conceived, designed, and coded in two hours one morning while suffering from ennui.

Magic Cookies and Other Nonsense

The idea behind any protection scheme is that of the software license, i.e. the developer or company allows you to use a preset number of copies of the software. The user should not be allowed to exceed this number in any situation. If software were something tangible then this might be enforcable, but since software is really a collection of analog signals transferred via magnetic media enforcing this is more than a trivial matter.

What the now infamous dongle did was make a part of the software tangible through a hardware device which attaches to the parallel port of your PC and is software addressable, to insure its presence. If the dongle isn't there the software displays an appropriate message and stops.

Illusions of Association

What makes the dongle successful (and annoying and the same time) is the bond, the association between the dongle and the software - if the dongle isn't there then the software will not continue. It then becomes our challenge to discover some method of binding the software with something that is unchangeable (which implies a hardware-assisted solution) or something that is very difficult to change (which can be done entirely through software).

So what shall we use as our magic cookie, the item which needs to be present in order to allow our application to run? The answer to this was nothing obvious and was discovered only by following a hunch I had one morning when pondering the CmnFilFormatDiskette() code that I had written some time ago.

Try this exercise in futility:

  1. Format a diskette and give it a label.
  2. DISKCOPY the formatted diskette so that you now have two blank, just-formatted diskettes.
  3. Examine both diskettes and discover the one difference between them that can be exploited under OS/2.
 [E:\]dir a:
 
  Volume in drive A is TESTDISK
   The Volume Serial Number is 25C2:5415
    Directory of A:\
 
 TEST     DAT       70  11-14-94  10:09a
          1 file(s)         70 bytes used
                       1455616 bytes free
 
 [E:\]dir a:
 
  Volume in drive A is TESTDISK
   The Volume Serial Number is 226D:EC14
    Directory of A:\
 
 TEST     DAT       70  11-14-94  10:09a
          1 file(s)         70 bytes used
                       1455616 bytes free
 
 [E:\]
Sleight of Hand
The answer, for those who cannot see it, is the volume serial number (VSN). Even if you use DISKCOPY to copy one diskette to another, the VSN is changed on the target diskette. As you will see, this is the key to the entire solution.

The Principle of Contagion

"Once together, always together," wrote Lyn Hardy in the series Master of the Five Magics and this is what we want to accomplish using the VSN as our magic cookie. Obviously, this means we have to tuck the VSN somewhere where the potential hacker cannot find it. And, while we're at it, we might as well put the number of available licenses remaining and the volume label of the diskette in our holding place also. But where do we put this information? An ASCII file wouldn't work because it is too easily cracked; a binary file would be better, but an inquisitive hacker could decypher the meaning of the data, once the purpose of the data was discovered. What we need is a way to store this important information in such a way that the data is never noticed, invoking the "Axiom of Forgetfulness" - "out of sight, out of mind." <grin>

Bag of Holding

Like the Advanced Dungeons and Dragons item, we need a place to put this information that is out of sight of any hackers. What better place to put it than in the extended attributes of a file? This is the best place, that I could think of, to put it.

Note
originally, I thought I could attach EAs to the root-directory of the diskette, like you can a subdirectory, but that returned ERROR_ACCESS_DENIED, so I had to settle on attaching them to the EAs of a hidden file instead.

Hackers, Start Your Keyboards

We now have enough information to write a complete function to handle installation diskettes. Let us look at the four actions that can be performed and the logic for each.

Create an Installation Diskette

  1. Use DosQueryFSInfo() to get the VSN and the volume label of the diskette to be initialized.
  2. Write the VSN, volume label, and the initial number of licenses to the EAs of a file on the diskette.

Decrementing the Number of Licenses

  1. Use DosQueryFSInfo() to get the VSN and the volume label of the "original" installation diskette.
  2. Read the EA data for the file on the diskette.
  3. Compare the information returned from DosQueryFSInfo() with that from the EA data and return an error if there is a mismatch.
  4. If there is are no more available licenses, return an error.
  5. Decrement the license count and write the EA data back to the file.

Incrementing the Number of Licenses

  1. Use DosQueryFSInfo() to get the VSN and the volume label of the "original" installation diskette.
  2. Read the EA data for the file on the diskette.
  3. Compare the information returned from DosQueryFSInfo() with that from the EA data and return an error if there is a mismatch.
  4. Increment the license count and write the EA data back to the file.

Determing the Number of Licenses

  1. Use DosQueryFSInfo() to get the VSN and the volume label of the "original" installation diskette.
  2. Read the EA data for the file on the diskette.
  3. Compare the information returned from DosQueryFSInfo() with that from the EA data and return an error if there is a mismatch.
  4. Return the number of available licenses.

Easy, huh? The code corresponding to the above is shown below, is provided in cpyprt.zip, and has also been integrated into Common/2 which will eventually be sent to hobbes.nmsu.edu.

 typedef struct FCIINSTALLINFO {
    ULONG ulSzStruct;
    CHAR achVolLabel[12];
    ULONG ulVolSerial;
    ULONG ulNumLicenses;
 } FCIINSTALLINFO, *PFCIINSTALLINFO;
 
 ULONG EXPENTRY CmnFilInstallDisk(PCHAR pchFile,
                                  PCHAR pchVendor,
                                  PCHAR pchApp,
                                  PCHAR pchName,
                                  PULONG pulNumLicenses,
                                  ULONG ulRequest)
 //-------------------------------------------------------------------------
 // This function performs one of the available actions on a diskette
 // which is intended to be an install diskette for an application.  The
 // action to be performed is specified in ulRequest:
 //
 //    CFID_REQ_CREATE - initialize the install diskette with a specified
 //                      number of licenses
 //    CFID_REQ_DECREMENT - decrement the number of licenses
 //    CFID_REQ_INCREMENT - increment the number of licenses
 //    CFID_REQ_QUERY - query the number of licenses
 //
 // On all actions but create, the diskette is checked to insure that
 // it is the original diskette initialized with the create action and
 // an error code is returned if not.
 //
 // Input:  pchFile - points to the name of a file to contain the
 //                   installation information.
 //         pchVendor - points to the name of the company producing the
 //                     product.
 //         pchApp - points to the name of the application.
 //         pchName - points to an arbitrary name.
 //         pulNumLicenses - if ulRequest=CFID_REQ_CREATE, this points
 //                          to a variable specifying the initial number
 //                          of licenses for this installation diskette.
 //                          Otherwise, it points to a variable to receive
 //                          the updated number of available licenses or
 //                          is NULL meaning that the information isn't
 //                          needed.
 //         ulRequest - one of the CFID_REQ_* constants
 // Output:  pulNumLicenses - if non NULL, this contains the updated number
 //                           of licenses after the request was performed.
 
 // Returns:  CFID_ERR_NOERROR if successful, a CFID_ERR_* constant
 otherwise.
 
 //-------------------------------------------------------------------------
 {
    APIRET arRc;
    CHAR achFullFile[CCHMAXPATH];
    CHAR chDrive;
    FSINFO fsiInfo;
    BYTE bNumDrives;
    ULONG ulVolSerial;
    USHORT usAttr;
    USHORT usSzData;
    FCIINSTALLINFO fiiInstall;
    BOOL bReturn;
 
    //----------------------------------------------------------------------
    // Get the fully qualified name of the file so that we can get the
    // drive letter.  FIL_QUERYFULLNAME does require the diskette to be in
    // the drive so that OS/2 can query the current directory on the drive.
    //----------------------------------------------------------------------
    DosError(FERR_DISABLEHARDERR);
 
    arRc=DosQueryPathInfo(pchFile,
                          FIL_QUERYFULLNAME,
                          achFullFile,
                          sizeof(achFullFile));
    if (arRc!=0) {
       DosError(FERR_ENABLEHARDERR);
       return CFID_ERR_DRIVENOTREADY;
    } /* endif */
 
    DosError(FERR_ENABLEHARDERR);
 
    chDrive=toupper(achFullFile[0])-'A'+1;
 
    //----------------------------------------------------------------------
    // Verify that the drive specified is a diskette drive.
    //----------------------------------------------------------------------
    DosDevConfig(&bNumDrives,DEVINFO_FLOPPY);
 
    if (chDrive>bNumDrives) {
       return CFID_ERR_NOTFLOPPY;
    } /* endif */
 
    //----------------------------------------------------------------------
    // Query the file system information on the diskette.
    //----------------------------------------------------------------------
    if (DosQueryFSInfo(chDrive,
                       FSIL_VOLSER,
                       &fsiInfo,
                       sizeof(fsiInfo))!=0) {
       return CFID_ERR_READFAILED;
    } /* endif */
 
    //----------------------------------------------------------------------
    // Get the volume serial number of the diskette.
    //----------------------------------------------------------------------
    ulVolSerial=*((PULONG)&fsiInfo.fdateCreation);
 
    usAttr=EAT_BINARY;
    usSzData=sizeof(fiiInstall);
 
    switch (ulRequest) {
    case CFID_REQ_CREATE:
       //-------------------------------------------------------------------
       // pulNumLicenses cannot be NULL if we are initializing the diskette.
       //-------------------------------------------------------------------
       if (pulNumLicenses==NULL) {
          return CFID_ERR_BADPARM;
       } /* endif */
 
       //-------------------------------------------------------------------
       // Setup the intended EA data and write it to the file.
       //-------------------------------------------------------------------
       fiiInstall.ulSzStruct=sizeof(fiiInstall);
       strcpy(fiiInstall.achVolLabel,fsiInfo.vol.szVolLabel);
       fiiInstall.ulVolSerial=ulVolSerial;
       fiiInstall.ulNumLicenses=*pulNumLicenses;
 
       bReturn=CmnFilSetExtAttribute(achFullFile,
                                     usAttr,
                                     pchVendor,
                                     pchApp,
                                     pchName,
                                     (PCHAR)&fiiInstall,
                                     usSzData);
       if (!bReturn) {
          return CFID_ERR_WRITEFAILED;
       } /* endif */
       break;
    case CFID_REQ_DECREMENT:
       //-------------------------------------------------------------------
       // Get the current information from the file.
       //-------------------------------------------------------------------
       bReturn=CmnFilQueryExtAttribute(achFullFile,
                                       pchVendor,
                                       pchApp,
                                       pchName,
                                       &usAttr,
                                       (PCHAR)&fiiInstall,
                                       &usSzData);
       if (!bReturn) {
          return CFID_ERR_READFAILED;
       } /* endif */
 
       //-------------------------------------------------------------------
       // If the data is incorrect or the volume serial numbers do not
       // match, then this isn't the original install disk.
       //-------------------------------------------------------------------
       if ((fiiInstall.ulSzStruct!=sizeof(fiiInstall)) ||
           (strcmp(fiiInstall.achVolLabel,fsiInfo.vol.szVolLabel)!=0) ||
           (fiiInstall.ulVolSerial!=ulVolSerial)) {
          return CFID_ERR_NOTINSTALLDISK;
       } /* endif */
 
       //-------------------------------------------------------------------
       // Insure that we have at least one more license left.
       //-------------------------------------------------------------------
       if (fiiInstall.ulNumLicenses==0) {
          return CFID_ERR_NOMORELICENSES;
       } /* endif */
 
       //-------------------------------------------------------------------
       // Update and write the new information back to the diskette.
       //-------------------------------------------------------------------
       fiiInstall.ulNumLicenses--;
 
       if (pulNumLicenses!=NULL) {
          *pulNumLicenses=fiiInstall.ulNumLicenses;
       } /* endif */
 
       bReturn=CmnFilSetExtAttribute(achFullFile,
                                     usAttr,
                                     pchVendor,
                                     pchApp,
                                     pchName,
                                     (PCHAR)&fiiInstall,
                                     usSzData);
       if (!bReturn) {
          return CFID_ERR_WRITEFAILED;
       } /* endif */
       break;
    case CFID_REQ_INCREMENT:
       //-------------------------------------------------------------------
       // Get the current information from the file.
       //-------------------------------------------------------------------
       bReturn=CmnFilQueryExtAttribute(achFullFile,
                                       pchVendor,
                                       pchApp,
                                       pchName,
                                       &usAttr,
                                       (PCHAR)&fiiInstall,
                                       &usSzData);
       if (!bReturn) {
          return CFID_ERR_READFAILED;
       } /* endif */
 
       //-------------------------------------------------------------------
       // If the data is incorrect or the volume serial numbers do not
       // match, then this isn't the original install disk.
       //-------------------------------------------------------------------
       if ((fiiInstall.ulSzStruct!=sizeof(fiiInstall)) ||
           (strcmp(fiiInstall.achVolLabel,fsiInfo.vol.szVolLabel)!=0) ||
           (fiiInstall.ulVolSerial!=ulVolSerial)) {
          return CFID_ERR_NOTINSTALLDISK;
       } /* endif */
 
       //-------------------------------------------------------------------
       // Update and write the new information back to the diskette.
       //-------------------------------------------------------------------
       fiiInstall.ulNumLicenses++;
 
       if (pulNumLicenses!=NULL) {
          *pulNumLicenses=fiiInstall.ulNumLicenses;
       } /* endif */
 
       bReturn=CmnFilSetExtAttribute(achFullFile,
                                     usAttr,
                                     pchVendor,
                                     pchApp,
                                     pchName,
                                     (PCHAR)&fiiInstall,
                                     usSzData);
       if (!bReturn) {
          return CFID_ERR_WRITEFAILED;
       } /* endif */
       break;
    case CFID_REQ_QUERY:
       //-------------------------------------------------------------------
       // Get the current information from the file.
       //-------------------------------------------------------------------
       bReturn=CmnFilQueryExtAttribute(achFullFile,
                                       pchVendor,
                                       pchApp,
                                       pchName,
                                       &usAttr,
                                       (PCHAR)&fiiInstall,
                                       &usSzData);
       if (!bReturn) {
          return CFID_ERR_READFAILED;
       } /* endif */
 
       //-------------------------------------------------------------------
       // If the data is incorrect or the volume serial numbers do not
       // match, then this isn't the original install disk.
       //-------------------------------------------------------------------
       if ((fiiInstall.ulSzStruct!=sizeof(fiiInstall)) ||
           (strcmp(fiiInstall.achVolLabel,fsiInfo.vol.szVolLabel)!=0) ||
           (fiiInstall.ulVolSerial!=ulVolSerial)) {
          return CFID_ERR_NOTINSTALLDISK;
       } /* endif */
 
       if (pulNumLicenses!=NULL) {
          *pulNumLicenses=fiiInstall.ulNumLicenses;
       } /* endif */
       break;
    default:
       return CFID_ERR_BADPARM;
    } /* endswitch */
 
    return CFID_ERR_NOERROR;
 }

Summary

What has been presented is simply an enabler. The code to call this function is still the responsibility of the application developer.

  • When you are installing the application, call CmnFilInstallDiskette() specifying CFID_REQ_DECREMENT to decrement the license count and check the return code.
  • When you are uninstalling the application, call CmnFilInstallDiskette() specifying CFID_REQ_INCREMENT to increment the license count and check the return code.

Limitation

You probably have already noted a major limitation of this scheme: since the VSN of the install diskette is stored in the EAs, the preparation of the install diskette cannot be done before duplication, but must be done - diskette by diskette - after duplication has been completed. The makes distribution of commercial software using this scheme much more cumbersome, but you did want that money, didn't you?

Also, it is fairly easy to hack the VSN of a diskette if the pirate does enough research to find out where on the diskette it is stored. This is the weakest point in the scheme, but given that the data could be stored anywhere, there is nothing to indicate that it is this scheme which is being used, so your application should still be fairly safe.

Feedback on this article will be greatly appreciated.