CrypKey 5.4 and the Power of (Self-) delusion

"Take a cool glass of your favourite drink, sit back, and read this superb essay very slowly, take delight as exefoliator walks you through the CrypKey codewoods from start to finish, even lending time to fix the developers bugs (are you listening CrypKey developers? ;-) ). This is exactly the kind of reverse engineering paper I want to publish in 2003, if you use the information here I think exefoliator certainly deserves some credits. Enough from me, let's watch the show, I warn you though, it won't be pretty if you are using CrypKey as protection right now ;-)". "Essay not edited very much by CrackZ and sorry exefoliator for the publishing delay ;P".

By exefoliator, August 2002.

Download CKKeyGen (66k).


CrypKey is a software protection and licensing suite that is apparently in fairly widespread use. Its purveyors claim that it is a superior product. This article evaluates this claim by examining what CrypKey does and, more importantly, how it does it. These investigations cast serious doubt on the strength and integrity of the security that CrypKey uses for protecting licence information. The structure, cryptography and meaning of the Site Code/Site Key pairs is examined in detail, as are those of the licence files. The information contained herein is sufficient to enable the preparation of a generic CrypKey Site Key generator, and includes some pseudo-code routines that clarify the operation of the required functions. In the broader context, the ruminations in this article lend much weight to the thinking that off-the-shelf software protection solutions invariably are poor ones and not worth the expense.

1. Introduction

Kenonic Controls Ltd. produce and supply a software protection and licence control system called "CrypKey". A visit to their web-site would seek to convince you that CrypKey is "battle-proven and the #1 Security Solution" and that you should "Build your Defense using only the Best". As will shortly become clear, this is over-hyped tripe of the purest strain. If CrypKey is the best on offer, then the product you wish to protect with it is already in deep trouble.

By my nature, I am anti-authoritarian and inquisitive, so when someone pretends to have a handle on something meaningful by making flashy claims, I take an interest, perhaps to the extent of being pathological, in finding out just how valid those assertions really are. CrypKey turned up as a software protection option during a web search for such stuff, although I had unwittingly encountered it previously in a web resource downloader program called "PaqRat". After reading CrypKey's info, I was moved to find out more about this product and downloaded the demo kit, which allows a 30-day evaluation period and consists of the protection program "CrypKey Instant 5.4" - henceforth "CKI" - and "CrypKey Site Key Generator 5.4 (Master)" - henceforth "SKW". After poking around the innards of both programs, I came to the conclusion that the lads at Kenonic are either unscrupulous for attempting to rip off (look at their prices!) or mislead (potential) customers, in which case they shouldn't be in the business of software security, or in desperate need of a long overdue reality-fix, in which case they shouldn't be in the business of software security. In either case, delusion seems to be their chief stock-in-trade, hence the title of this article.

CrypKey probably got its name from the fact that it uses cryptographic routines to protect licence information. I suspect that it may have its roots in some archaic UNIX software because at the interface level of the protection module(s), function arguments are often ASCIIz strings (usually of hexadecimal digits), even when the data the function ultimately operates on are the binary translation of the string. Also, these "byte" strings in several cases contain big endian fields, contrary to the x86 processor's native format. This pair of facts alone should set the "dodgy implementation" flag bit.

In essence, CrypKey's security and protection rests on two aspects. The first of these is its use of the hard disk as a playground in which to hide things from the rest of the world. The first part of this article provides a fairly comprehensive discussion of the reasons for, as well as the nature of this hard disk usage. The second aspect relates to the licensing functionality and cryptography of CrypKey, and is examined in depth in the second part of the article.

2. The Summa Discologica

This section will describe conceptually some of the low-level operational details of the first part of the protection mechanism used by CrypKey. The emphasis is on the disk activity that occurs on a system that has been "infected" with CrypKey. The reader should note that the investigations were carried out on an Intel PIII running Windows 98, and hence most of the information which follows regarding disk usage by CrypKey does not apply in the case of NT-based systems (i.e. WinNT, Win2k and WinXP). NT-based systems use an entirely different mechanism, even if all of the available fixed disk partitions are formatted FAT rather than NTFS, but more on this later.

The first strange thing I noticed was that after installing a CrypKey protected program (in this case CKI & SKW), firing it up for the first time caused a message dialogue to briefly pop up, saying something about checking the eligibility for a trial licence. This dialogue is difficult to read, let alone capture, due to its brief appearance. It is shown on the first run and again when the trial licence expires or the licence files are deleted, moved or modified. The strange part is that when one tries to install a second, third, fourth, etc. copy of the protected program, the later copies all somehow know that a prior copy already exists on the machine.

Uninstalling and reinstalling doesn't give you a new trial licence either, even if you take the trouble of preparing a system snapshot (disk files & registry) prior to installation so that you can manually "restore" the system to what it was when the trial expires. Apparently, the only way to obtain a fresh trial licence is to format the hard disk and reinstall everything from scratch, which definitely isn't a very pretty or practical solution. I haven't tested whether or not ghosting the machine works, but, for reasons that will be clear shortly, this tactic will only work if the ghosting software does a complete sector-by-sector rebuild of the hard disk drive on which the program was originally installed, and/or you happen to be exceptionally lucky.

So how do they do it? Well, they use two tricks, one brash and presumptuous, the other (on FAT systems) subtle and potentially dangerous to the good health of your PC. The first trick is to scatter a few - ten or so - small binary files all over the drive in random locations. These files have odd names, e.g. "PFFF.JDD", "LH", "QSB.FG", "ESQR.A", etc., contain the same data, and are always an exact multiple of four bytes in size. A CrypKey protected program that has run at least once on the system has a corresponding four-byte (dword) entry in each of these signature files.

The dword entries in the signature files appear to be ordered according to the sequence in which the protected programs were installed, i.e. the signature of the last installed product is the last entry in the signature files. In addition, CrypKey always creates the file "C:\IOU.SYS" with the system and hidden attributes set, which contains the same data as the other signature files and makes it easier to find the other ones. Yet another file, "C:\Windows\System\jsm51161.tbl", is a two-byte file of unknown purpose, possibly some form of global system signature or time stamp. Deleting it does not seem to have an effect - it just gets recreated, albeit with different data. As an aside, it is quite possible that "jsm" are the initials of one Jim McCartney, who seems to be a big fish at Kenonic, and that he was born on 5 November 1961 - those who believe in and know of things astrological might be able to reconcile this birthday with a personality that appends the coda "Product Visionary" to its name...

The second trick is to use the slack space at the end of some disk files - again ten or so - to plonk down some more dword signatures. Let me explain this idea a little. Suppose your hard disk drive is formatted with 4 kB clusters (allocation units) and you have a file that only uses, say, 2.5 kB. The FAT file system will correctly report the file's size as 2.5 kB (the file's proper size), but the file actually eats up 4 kB (one cluster) of disk space, with 1.5 kB of slack. With the same disk geometry, a 27.6 kB file will use up 7 clusters = 28 kB and have 0.4 kB of slack. The slack space only comes into play when a file's proper size changes. CrypKey analyses directory entries in conjunction with the (first) FAT table so as to identify such slack space behind EXE and COM files (for which size changes are unlikely) and performs lots of cloak-and-dagger absolute disk IO to read and write signatures, most of which you'll only find with a sector-level disk editor.

I said earlier that this is potentially dangerous. You see, the folks at Kenonic haven't done their homework properly, at least up to and including CrypKey v 4.3, which served as the test bed in this case. I found two files ("C:\Windows\System\Oobe\Msoobe.exe" and "C:\Windows\System\Viewers\Quikview.exe") with the signatures INSIDE the files. These two files are 36864 and 28672 bytes, respectively, and if you do the calculations, you will find that these sizes translate to exactly nine and exactly seven clusters, respectively. Yep, their slack space calcs screw up royally when eyeballing a file that fits exactly into an integer multiple of disk clusters, and will, without any regard at all, corrupt the file, with potentially disastrous consequences, especially in the case of a COM file. From a practical perspective, although the chances of this actually happening are quite remote, it is certainly not impossible.

With regard to the naming and locations of the signature files and the locations of the slack space signatures, I have not investigated in any great depth (1) how CrypKey decides where to put the signature files, (2) how it generates the dword product signatures in these files, (3) how it generates the names of the signature files, (4) how it decides where to put the slack space information, or (5) exactly what the meaning of the slack space information is. In partial answer to some of these questions, it seems that the locations of the signature files as well as the slack space signatures are decided primarily by the size of the disk partition and how much of it has been used. CrypKey doesn't create any new directories; rather, the signature files are dropped into pre-existing ones. Assuming the predominant sector size of 512 bytes, the slack space dword signature is written at offset 504 (0x1F8h) of the last sector of the last cluster occupied by the affected file. On Win9x/ME systems, the command-line interpreter, normally "C:\COMMAND.COM" (which isn't actually a bona fide COM file), invariably seems to be a target, which makes sense if paranoia is your game, dunnit? The reader is encouraged to take a stab at shedding a bit more light on these questions, although the information would be mostly of academic interest.

Except for one little problem, the idea of using file slack space would also work on NTFS partitions since this file system also uses allocation units or clusters and, at the physical disk level, deals with them in a similar fashion to FAT, i.e. a file always occupies an integer multiple of clusters. But before one hits the brick wall that is NTFS, there is a prior problem to be overcome. NT-type systems require that a user is logged on with administrator privileges before the system grants the NT equivalent of direct disk access, where the entire physical disk or partition appears as a large, flat file. However, users are not always logged on with sufficient privileges, and thus it is necessary that the direct disk access is provided as a service, e.g. in the local system account. This is easy enough to do, and CrypKey on NT systems includes a service, but it doesn't do any direct disk IO. The real problem is that NTFS has the ability to transparently (i.e. you don't even know about it) relocate, with the exception of the bootstrap code, any and all on-disk structures on the fly should this become necessary for any reason, e.g. a bad sector. However, given my reservations about the technical competence of the Kenonic crowd, it is, I submit, more likely that they haven't implemented a similar slack space device on NTFS because they simply don't know how NTFS works. Frankly, CrypKey's NT implementation sucks worse than the Win9x/ME variety.

Direct disk access on Win9x/ME by protected mode 32-bit code although possible, is fairly cumbersome to implement. CrypKey does such disk IO via a flat thunk from a 32-bit DLL to a 16-bit DLL. In fact, the 16-bit DLL implements all of the licence management functionality required by CrypKey, while the 32-bit DLL merely provides the 32-bit interface. These DLLs usually live alongside the protected application and are named "Cryp95?.dll" (32-bit) and "Crp9516?.dll" (16-bit), where the "?" is a letter of the alphabet - "e" in the case of CrypKey 5.4 - and is presumably some type of version identifier. I suspect that the 16-bit library is legacy code that happened to have, from a security point of view, the additional advantage of being a bit of a swine to run and/or debug interactively within 32-bit code. In earlier versions of CrypKey it was an almost trivial exercise to patch the 32-bit side on Win9x/ME so as to dupe the security layer into behaving as though a valid licence was present. Later versions wrap up a copy of the 16-bit DLL as a binary resource in the protected module and extract it on demand, but it only gets used to verify the integrity of the existing 16-bit library. Although I have not tried to do so, it may still be feasible to patch the 32-bit library without CrypKey being any the wiser.

Apart from providing a mechanism to detect other instances of an installed product via the slack space signatures and signature files, the direct disk access allows a further security measure. A quick examination of the licence files, which have the extensions "41s", "ent", "rst" and "key" and normally live with the protected program in the same directory, reveals that each of these has both the system and hidden file attributes set. The hidden attribute is fairly meaningless if you set up your Windows Explorer to show hidden files, but the system attribute tells the FAT file system driver that it may not move the file. This is borne out when one makes copies of the licence files in a specific directory, deletes the originals, and renames the copies to their original names - CrypKey doesn't like that at all on Win9x/ME PCs, and wails about a "restriction file" that was moved. Even worse, your licence is destroyed beyond redemption. Doing so on an NT machine, even with a FAT disk, does not provoke the same dramatic reaction, as long as the files are not manually moved to a different drive and/or directory, in which case a similar result ensues. So what's going on here? Well, CrypKey binds the "rst" and "key" licence files to specific allocation units on your disk and stores this location info (actually an abstract thereof) as part of the "ent", "key" and "rst" licence files. In most circumstances, setting a file's system attribute flag ought to be sufficient to prevent its being moved. However, there is at least one well-known disk defragmenter that does not always respect this flag, which means that you can lose your licence(s) without a clue as to how or why it happened.

On NT systems this aspect is a bit more lax because manipulating disk clusters is something that Micro$oft actively discourages, particularly in the case of an NTFS volume. Kenonic did a bit of a cop-out here, and the licence files are bound to a specific directory (by way of its name) on a specific hard disk partition (by way of its 32-bit volume serial number), rather than to specific disk clusters. This explains why on an NT system you can move the licence files around in their parent directory with impunity. However, if you decide that renaming a few installation paths is a fine way to while away a boring Saturday afternoon, you're in for a big surprise. More insidious still is what happens when you use some utility to alter the volume serial number of your "C:\" drive (yep, the name's hard-coded into the NT service that CrypKey installs) because you're entitled to believe that a volume serial number like "B00B-FACE" reflects more accurately your take on the world of disk drives than does, say, "3E15-16D4". Since NT caches such disk info, you will still be a properly licensed user until you reboot the machine.

When a CrypKey protected product is installed on an NT system, the setup program looks for a set of NT drivers for CrypKey, and installs or updates them if they do not exist or are too long in the tooth. These drivers consist of a service called "Crypkey License" and a system driver, both of which lead positively innocuous and quiet lives in the "%SystemRoot%\System32" directory (usually "C:\WinNT\System32" or "C:\Windows\System32" on WinXP) as files named "CrypServ.exe" and "CKLDrv.sys", respectively. In "%SystemRoot%" you will also find two other executables that are part of CrypKey's NT stable, namely "CKConfig.exe" and "CKRfresh.exe". The former of these is a configuration utility that allows editing of the list of directories that CrypKey should keep tabs on, i.e. directories that contain licence files and/or protected programs, while the latter is used to pump a notification code to the service that tells it to refresh itself, if, e.g., a configuration change has been made.

The service provides an interface to the driver that allows protected applications to query licence information and whether or not the licence and/or protected files have been moved. It does nothing more than that. The verification of the licence details, i.e. the contents of any licence files, is done either by additional code injected into the protected module or by another library ("CKI32h.dll" in the case of CKI). To prevent or limit reversing, this verification code makes much use of stealth techniques that are, if the truth be told, excellent. This is perhaps the only aspect of CrypKey that Kenonic need not be ashamed of trumpeting about. But going back to the way things work on NT, the driver, service and licence checking code communicate via small, short-lived and trivially encrypted binary disk files in the directory that houses the relevant licence or protected files. These files have strange names such as "5411387._rq" and "5411387._an". The names consist of a seven-digit time stamp value (the number that precedes the extension), followed by an extension that identifies whether the file is a request ("_rq") or an answer ("_an") file.

A further communication file type that has the extension "_tb" - I don't know what the "_tb" means - comes into play when one tries to license a protected program by entering a Site Key, but this type uses the name of the protected program instead of a time stamp, e.g. "CKI._tb". The part that really sucks about all of this is that the service repeatedly searches the watched directories for the appearance of files with any of these extensions. If it finds any, it processes them according to their type, goes to sleep for a while to give the protected program a chance to use them, and then deletes them. If no such file is found, it also goes to sleep for a while (between 100 and 163 milliseconds, inclusive), wakes up and tries all over again. The protected application generates the "._rq" request files, sleeps, wakes up, and looks for the corresponding "._an" answer files. If the service is not running, the program will, of course, not get any replies and will retry a few times, eventually terminating with an error. All of this makes me wonder whether these guys have ever heard of named pipes or DDE. The service also maintains two encrypted index files ("esnecil.ind" and "esnecil.nlp" - read the names backwards) in the "System32" folder, which CrypKey uses to keep track of the names, locations and other details of all the protected programs that are installed on the system. Each program's index entry in these files is 560 (0x230h) bytes long, and the file with the "nlp" extension is a backup that is created by later versions of CrypKey when installed over an earlier one. The "nlp" file extension probably stands for "no long pathnames" because earlier versions only recognised DOS 8.3 paths, while later versions of the service incorporate code that obtains the true (long) Win32 path and file name from its 8.3 name, provided it exists on the system.

The system driver also references the index file and maintains a system time stamp that is saved in several places. The first of these is inside the index (".ind") file itself whenever this is accessed, which is hardly surprising. Somewhat more obscure and difficult to catch is the creation and updating of two dword registry values under the "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion" hive ("CurrentBuildId " and "SystemType " - note the spaces). Trickier yet is the fact that the driver ("CKLDrv.sys") writes to its own MZ/PE header disk file image each time the service sends it a control code. A seven-byte time stamp is written immediately following the string "This program cannot be run in DOS mode.<CR><CR><LF>$" in the driver's DOS stub (remember, DOS strings are terminated with the "$" character), and the 32-bit File Checksum field in the PE header (at offset 0x58h) is also updated so that the checksum is correct.

Unfortunately, there is still at least one piece of the NT puzzle missing because stopping the service, deleting the index and licence files, cleaning the registry and driver file (not easy, that) and restarting the service does not mean CrypKey will grant you a new trial licence. I have looked into a number of possibilities that might explain this behaviour but, thus far, without any success. The driver does not modify the protected program's disk image. Nor does it use multiple file streams which NTFS supports. There is no indication that I can find of any slack space manipulations, yet CrypKey manages to put something somewhere on the disk by which it can tell whether or not an attempt at crooking it is made.

The sneaky bit about CrypKey is that uninstalling a product does not clear the PC of all traces of the installation, as by rights it should. On Win9x/ME computers the signature files and slack space signatures remain, while on NT systems the NT service, system driver and index file(s) are never removed. In fact, the service is set to start up automatically and will continue to run, even if there isn't any CrypKey protected program installed on the system at all. The task of restoring a system to its original state is left to the hapless user or administrator, and if you've ever been faced with a recalcitrant system driver that seriously isn't keen on the idea of giving up its hold on your PC, you'll know that this task can cause several sorts of grief. The essence of CrypKey is that it infests and infects any hard disk drive much like a cancer, and that it is similarly pleasant and easy to get rid of.

All of the disk activity described above - those annoying, disk-munching signature files and the slack space shenanigans - happens without your knowledge and, worse, without your consent. The CrypKey licence agreement doesn't warn you that you run the risk of file corruption (Win9x/ME) or performance degradation (WinNT/2k/XP) if you proceed with the installation. It does not mention the fact that uninstalling the software will not remove every trace of the installed product and restore your computer to what it would have been had you never installed the thing in the first place. Nowhere does it warn of the potentially negative consequences that using a disk defragmenting utility may have. It is, of course, your own choice to install the product or not, but the licence agreement must, by my way of thinking, provide one with adequate information in order to make an appropriately informed decision, a point on which it fails abominably because these facts are omitted. This is not a suggestion that they reveal their trade secrets - someone such as I will do that for them - it is more one that they be a bit more honest and forthright. But, as always, it's a case of "caveat emptor".

3. A Tale of Two Cryp Keys

After that fairly exhaustive (exhausting?) exposition of the disk activities that accompany an instance of a CrypKey protected program, this section will attempt to describe the second, and more important part of the protection mechanism, viz. the cryptography behind CrypKey. If the reader is unfamiliar with the inner mechanics of the RSA cryptographic technique and/or the principles behind stream ciphers, it would be a good idea for him or her to become familiar with both of these concepts before continuing, since these constitute the twin foundations on which CrypKey's licensing functionality is built. The reader is also cautioned that this section is more technical than the preceding one and is certainly more important, since it renders a knowledge of CrypKey's disk manipulations almost completely redundant if the sole intention is to defeat CrypKey's security.

For the purpose of subsequent explanations in this article, I define the following two function prototypes :

(1) CRYPT_RSA(BaseNum, KeyExp, KeyMod)
(2) CRYPT_STREAM(DataIn, Direction, StreamKey)

The implementation details of these functions are given in the appendix.

The "BaseNum" argument in CRYPT_RSA is (a portion of) the data block that is to be RSA coded and should always be numerically less than the "KeyMod" argument, which, together with the "KeyExp" argument, makes up the RSA key. "KeyExp" and "KeyMod" are the exponent and modulus parts of the key, respectively. The reader may have noticed that CRYPT_RSA does not provide any explicit means for defining encryption or decryption. Being an asymmetric cipher, RSA uses different keys for encrypting and decrypting, so that the terms "en-" and "decryption" are inherently defined by the input data and the RSA key. In CRYPT_STREAM, the "DataIn" argument represents a block of contiguous bytes, and it should be noted that the size of the block in bytes is assumed to be implicit in the argument itself, i.e. from its length. The "Direction" parameter can take on one of two possible values, namely FWD (forward) and REV (reverse). One may be tempted to think that FWD means encryption and REV means decryption, but this conception can be misleading; it is more convenient to think of them as complementary operations, such as addition and subtraction, i.e. the one will undo what the other does. The CRYPT_STREAM parameter "StreamKey" is a sequence of bytes that serves as the key for en- or decoding data, and normally is the same for FWD and REV. CrypKey makes use of stream key blocks that are four bytes long. Finally, the functions will return processed data that is of the same type as the data they operate on, i.e. a block of contiguous bytes in the case of CRYPT_STREAM and a number in the case of CRYPT_RSA. These facets are illustrated conceptually by the following examples :

CRYPT_RSA(0x6F80h, 1069, 123107) = 0x12A3Dh
CRYPT_STREAM(30EC90A504F97B, FWD, E467F6A3) = E67F29F1776E3B

The above examples also illustrate some of the notational conventions that I will use throughout, viz. (1) data blocks are given as hexadecimal byte values without the "0x" prefix and "h" suffix, which distinguishes them from hexadecimal numbers, and the byte values are in left-to-right (reading) order, with each byte occupying a pair of hex digits, and (2) an ordinary number is represented in either decimal or hex notation, where in the latter case the "0x" prefix and "h" suffix are used.

These two functions comprise the core of CrypKey's licensing security. With them, it is possible to construct a single generic CrypKey function that encompasses all of the functionality required to analyse the data CrypKey uses in licensing, such as the content and meaning of the licence files, the structure and meaning behind the Site Code and Site Key, etc. However, before delving into the details of such a function, a few remarks are in order about Kenonic's implementation of our mate, the CRYPT_RSA function, as well as the keys they use.

The first, and probably most shocking aspect is that the blokes at Kenonic suffer under the misapprehension that computational efficiency is at its highest when one ports a problem that resides entirely, unreservedly and indisputably in the domain of natural number (i.e. positive integer) arithmetic to the more general domain of floating-point number arithmetic and then throws in some recursion as well. Yes, that is just what they have done, and is illustrated in the CRAPPY_RSA pseudo-code below. Note that the "fp_" prefix is used to emphasise that the relevant variable is an IEEE-754 double precision (64-bit) floating-point type :

  CRAPPY_RSA(fp_BaseNum, fp_KeyExp, fp_KeyMod)
    fp_TempProd <- 1.0
    If (Truncate(fp_KeyExp) <= 3) then
      For Remainder = 1 to Truncate(fp_KeyExp) do
        fp_TempProd <- fp_TempProd*fp_BaseNum
      For Remainder = 1 to (Truncate(fp_KeyExp) MOD 3) do
        fp_TempProd <- fp_TempProd*fp_BaseNum
      fp_TempProd <- fp_TempProd-fp_KeyMod*Truncate(fp_TempProd/fp_KeyMod)
      fp_TempProd <- Truncate(fp_TempProd)
      fp_TempResult <- CRAPPY_RSA(fp_BaseNum, Truncate(fp_KeyExp/3), fp_KeyMod)
      fp_TempResult <- fp_TempResult*fp_TempResult*fp_TempResult
      fp_TempResult <- fp_TempResult-fp_KeyMod*Truncate(fp_TempResult/fp_KeyMod)
      fp_TempResult <- Truncate(fp_TempResult)
      fp_TempProd <- fp_TempProd*fp_TempResult

Now isn't that just plain awful? Even on a system with a numeric coprocessor, the above CRAPPY_RSA function on average runs more than six times slower for the same input than CRYPT_RSA as given in the appendix, which uses the CPU's native integer arithmetic capabilities. There are comparatively large penalties in the data type conversions from integer to floating-point, the truncations that convert floating-point types to integers, as well as in the required parameter passing that occurs whenever the function recursively calls itself. Other than the habitual infusion of unsafe doses of folly juice, I can think of only one other possible explanation for such a lamentable coding fiasco, namely willful and mendacious obscurantism: it is not a trivial exercise to extract the actual value of a floating-point variable if all one has is its binary form, but any worthy debugger will make short work of this, and tracing through recursive code can become a bit tedious.

The use of RSA is in principle a good idea because it is asymmetric, i.e. encoding requires a different, but related, key to decoding. This means that the integrity of encoded information can be preserved, provided that one member of each key pair remains a secret. This was the original intention of the RSA scheme, which lends itself well to software licensing because the licence information is RSA encrypted with the secret key, and the verification routines need only be aware of the other (public) key. Therefore, the licence verification will not reveal the secret key, and thereby complicate the task of faking licence information.

Kenonic slipped up badly on two counts in this regard. Firstly, their protection code reveals both members of the RSA key pair that applies to the Site Code, Site Key and licence files. More specifically, the Site Code is encoded with the same key as is used to decode the Site Key, but each of the licence files is encrypted using the key that ought to have remained secret. In order to clarify the second mistake, some background is required. Cracking an RSA code boils down to the task of factoring the key modulus into (two) prime numbers of roughly equal size. This sounds fairly innocuous and easy, but becomes enormously time-consuming for large numbers since the problem's solution space grows exponentially with the number of digits of the number that is to be factored. Even the fastest currently available factoring techniques sift the solution space for an answer, so that they are still essentially brute force searches. The performance of a factoring algorithm can be related directly to the level of sophistication that is applied in moderating the "brutishness" of the search. At present, the originators of RSA recommend that an implementation should have a modulus that is at least 1024 bits long (roughly 300 decimal digits), and 2048 bits is considered to be secure for banks, the military, governments and similar forms of institutionalised paranoia.

A further aspect of RSA is that its security, when compared with symmetric schemes like DES, Rijndael, Blowfish, etc., is only half the key modulus' bit length, i.e. on the assumption that RSA and Blowfish are both intrinsically perfect ciphers, a message encrypted with 512-bit RSA is equally secure to one encrypted with 256-bit Blowfish. To crack the Blowfish message, a search through a 256-bit key space is necessary (not to mention a really long wait). The RSA message will also require a search through a 256-bit key space, since one of the factors of the 512-bit key modulus must necessarily be less than or equal to the square root of the modulus, i.e. the factor must have 256 or fewer bits.

Having established all of the above, it surely must come as a shock to learn that Kenonic have opted for 17-bit RSA. That's a "one", followed by a "seven". It is the barrier between licensed and unlicensed software, if one forgets that Kenonic has already given away the secret key. In more pedestrian terms, the key modulus is no more than 131072, and factoring a number of this order is roughly a primary school level exercise - at worst, one would have to try a measly 72 possible prime factors.

CrypKey uses two different sets of RSA keys for different purposes. They are :

  RSA_Key1 = (KeyMod, PubExp, PrvExp) = (123107, 229, 1069) and
  RSA_Key2 = (KeyMod, PubExp, PrvExp) = (130771, 809, 2089).

Note that the keys are complete in the sense that the public and private parts are both present, so that we can merrily crypt back and forth without impediment.

The astute reader who is also familiar with CrypKey will perhaps have noticed that the RSA keys given above seem to be inconsistent with the length of the Site Code, Site Key and the information in the licence files, all of which are significantly longer than 17 bits. However, when encrypting a block of data, CrypKey divides it into a series of 16-bit chunks (words) and RSA encrypts each 16-bit chunk in turn. The result of CRYPT_RSA(16-bit chunk, KeyExp, KeyMod) will always be 17 bits long for either of the two keys given above. CrypKey tacks the 17th (most significant) bit of the encrypted chunk separately onto the front end of an initially empty bit string, and replaces the original 16-bit chunk with the low order 16 bits returned by the CRYPT_RSA function. After completing the last 16-bit chunk, the bit string containing all those 17th bits is finalised. If it is an exact multiple of eight bits in length, the string is left as it is, otherwise zeroed bits are added to it until its length is a multiple of eight bits, i.e. it is zero-extended to the next higher byte boundary. The encrypted series of low order 16-bit chunks is appended onto the end of the bit string, and the resulting block is then fed to the stream cipher machine, CRYPT_STREAM with FWD and StreamKey = E467F6A3. But what happens when the input data block is not an exact multiple of 16 bits in length? Without flourish or fanfare CrypKey just parks a zero byte at the end of the block.

Let's see this in action by way of an example :

  Given:    DataIn  = 437279704B6579207375636B73
  Required: CrypKey encrypt with RSA_Key1.PrvExp = (123107, 1069)
  Step(1)   DataIn -> 4372 7970 4B65 7920 7375 636B 7300
            (separated into 16-bit chunks, zero byte appended)
  Step(2)   CRYPT_RSA(0x4372h, 1069, 123107) = 0x14D8Dh (Bit 17 is high)
              BitString <- 1
              DataOut   <- 4D8D
            CRYPT_RSA(0x7970h, 1069, 123107) = 0x1593Dh (Bit 17 is high)
              BitString <- 11
              DataOut   <- 4D8D 593D
            CRYPT_RSA(0x4B65h, 1069, 123107) = 0x11EF2h (Bit 17 is high)
              BitString <- 111
              DataOut   <- 4D8D 593D 1EF2
            CRYPT_RSA(0x7920h, 1069, 123107) = 0x0A1E6h (Bit 17 is low)
              BitString <- 0111
              DataOut   <- 4D8D 593D 1EF2 A1E6
            CRYPT_RSA(0x7375h, 1069, 123107) = 0x172D8h (Bit 17 is high)
              BitString <- 10111
              DataOut   <- 4D8D 593D 1EF2 A1E6 72D8
            CRYPT_RSA(0x636Bh, 1069, 123107) = 0x05716h (Bit 17 is low)
              BitString <- 010111
              DataOut   <- 4D8D 593D 1EF2 A1E6 72D8 5716
            CRYPT_RSA(0x7300h, 1069, 123107) = 0x01AFDh (Bit 17 is low)
              BitString <- 0010111
              DataOut   <- 4D8D 593D 1EF2 A1E6 72D8 5716 1AFD
  Step(3)   BitString <- 00010111 = 0x17h
            (single zero bit added to make eight bits long, convert to byte)
  Step(4)   DataOut <- 17 4D8D 593D 1EF2 A1E6 72D8 5716 1AFD
  Step(5)   DataOut <- CRYPT_STREAM(174D8D593D1EF2A1E672D857161AFD, FWD, E467F6A3)
            DataOut = C1F99510B297F41AC81DAC346F6393
                    = C1F9 9510 B297 F41A C81D AC34 6F63 93

Probably the most important aspect to note in the above example is that the 16-bit input chunks are read and the low 16 bits of CRYPT_RSA's results are stored in big endian order, i.e. in reverse byte order to the native x86 format, which is little endian. This means that each 16-bit load, as well as each 16-bit store cycle will require either one 16-bit load/store plus one byte swap or two separate byte load/store operations prior to calling and after returning from CRYPT_RSA. Apart from that nasty floating-point RSA implementation, simple logic clearly marks this as a superfluous bonus because RSA does not in principle care a toss about the value of the "BaseNum" argument. Calling CRYPT_RSA(0x4372h, 1069, 123107) involves exactly the same computational effort as calling CRYPT_RSA(0x7243h, 1069, 123107), albeit that the results are different. Moreover, recovering the original "BaseNum" value from an encrypted one is not made any more easy or difficult by changing the order of the bytes. Thus, the benefits of doing so are quite obscure, but it alleviates having to comprehend the inordinately challenging notion that x86 processors store numbers larger than a single byte in back-to-front order.

A further point to note is that the encrypted output will in general always have a greater length, i.e. byte count, than the unencrypted input. Three lengths are of interest prior to encryption, viz. the size of the input, "ByteInLen", the size of the string of 17th bits, "CarryLen", and the size of the output, "ByteOutLen". On the assumption that each of these lengths is measured in bytes, the formulae below show the relationships among, and calculation sequence for these quantities in the general case when encrypting a block that has a byte count of "ByteInLen" :

  For encryption:
  (E.1) WordInLen <- Truncate((ByteInLen+1)/2)
  (E.2) CarryLen <- Truncate((WordInLen+7)/8)
  (E.3) ByteOutLen <- 2*WordInLen+CarryLen
  (E.4) if (2*WordInLen > ByteInLen) then append a zeroed byte to the input block
        (the difference can be at most one byte)

Needless to say, the encryption method illustrated above is completely reversible, but to highlight the differences between encryption and decryption, the next code sample will show the sequence when decrypting the data block obtained as output in the previous example. However, before continuing, a general method for obtaining the sizes described above will be required, i.e. to determine from its total size which portion of the encrypted block constitutes the string of 17th bits in order to decrypt the block correctly. An analysis of the formulae for encryption shows that certain encrypted block lengths are, strictly speaking, not permissible, e.g. 1, 2, 4, etc., but we can again resort to appending zeroed bytes onto the end of a block when its length is one of these illegal values. All we really need to know is that on average, 16 bits become 17 bits during encryption.

  For decryption:
  (D.1) OutLenEst <- Truncate((16*ByteInLen+1)/17)
  (D.2) WordOutLen <- Truncate((OutLenEst+1)/2)
  (D.3) CarryLen <- Truncate((WordOutLen+7)/8)
  (D.4) ByteOutLen <- 2*WordOutLen
  (D.5) DiffLen <- ByteOutLen+CarryLen-ByteInLen
        if (DiffLen > 0) then append DiffLen zeroed bytes to the input block
        (the difference can be at most two bytes)

We now have all the tools to do the decryption, so let's do it :

  Given:    DataIn  = C1F99510B297F41AC81DAC346F6393
  Required: CrypKey decrypt with RSA_Key1.PubExp = (123107, 229)
            (note: complementary RSA key to that used for encryption)
  Step(1)   TempBlock <- CRYPT_STREAM(DataIn, REV, E467F6A3)
                       = CRYPT_STREAM(C1F99510...346F6393, REV, E467F6A3)
                       = 174D8D593D1EF2A1E672D857161AFD (length = 15 bytes)
  Step(2)   OutLenEst <- Truncate((16*15+1)/17) = 14
            WordOutLen <- Truncate((14+1)/2) = 7
            CarryLen <- Truncate((7+7)/8) = 1
            ByteOutLen <- 2*7 = 14
            DiffLen <- 14+1-15 = 0
            (since DiffLen == 0, no zeroed byte padding is necessary)
  Step(3)   BitString <- 0x17h = 00010111
            (since CarryLen = 1, BitString = first byte of DataOut)
            TempBlock <- 4D8D 593D 1EF2 A1E6 72D8 5716 1AFD
            (BitString removed & separated into 16-bit chunks)
  Step(4)   Chunk <- 0x4D8Dh+0x10000h = 0x14D8Dh
              BitString <- 0001011
              CRYPT_RSA(0x14D8Dh, 229, 123107) = 0x4372h
              DataOut <- 4372
            Chunk <- 0x593Dh+0x10000h = 0x1593Dh
              BitString <- 000101
              CRYPT_RSA(0x1593Dh, 229, 123107) = 0x7970h
              DataOut <- 4372 7970
            Chunk <- 0x1EF2h+0x10000h = 0x11EF2h
              BitString <- 00010
              CRYPT_RSA(0x11EF2h, 229, 123107) = 0x4B65h
              DataOut <- 4372 7970 4B65
            Chunk <- 0xA1E6h+0x00000h = 0x0A1E6h
              BitString <- 0001
              CRYPT_RSA(0x0A1E6h, 229, 123107) = 0x7920h
              DataOut <- 4372 7970 4B65 7920
            Chunk <- 0x72D8h+0x10000h = 0x172D8h
              BitString <- 000
              CRYPT_RSA(0x172D8h, 229, 123107) = 0x7375h
              DataOut <- 4372 7970 4B65 7920 7375
            Chunk <- 0x5716h+0x00000h = 0x05716h
              BitString <- 00
              CRYPT_RSA(0x05716h, 229, 123107) = 0x636Bh
              DataOut <- 4372 7970 4B65 7920 7375 636B
            Chunk <- 0x1AFDh+0x00000h = 0x01AFDh
              BitString <- 0
              CRYPT_RSA(0x01AFDh, 229, 123107) = 0x7300h
              DataOut <- 4372 7970 4B65 7920 7375 636B 7300
  Setp(5)   DataOut <- 437279704B6579207375636B7300

The final output from the above decryption is, except for the trailing zero byte, the same as the input to the encryption.

It is now an almost trivial matter to combine the formulae and principles used in the above examples into a generic CrypKey function that performs both the en- and decryption required to make sense of the licence files, Site Code, SiteKey, etc., as used by CrypKey. This function's prototype, CRYPT_CRYPKEY, is given below with its implementation in pseudo-code :

  CRYPT_CRYPKEY(DataIn, CryptType, RSAExp, RSAMod)
    If ((RSAExp = 0) or (RSAMod <= 1) or (RSAMod >= 0x20000h)) then
    WorkBlock <- DataIn
    ByteInLen <- ByteCount(DataIn)
    If (ByteInLen = 0) then
    If (CryptType = ENCRYPT) then
      WordInLen <- Truncate((ByteInLen+1)/2)
      CarryLen <- Truncate((WordInLen+7)/8)
    Else If (CryptType = DECRYPT) then
      OutLenEst <- Truncate((16*ByteInLen+1)/17)
      WordInLen <- Truncate((OutLenEst+1)/2)
      CarryLen <- Truncate((WordInLen+7)/8)
      WorkBlock <- CRYPT_STREAM(WorkBlock, REV, E467F6A3)
      Bit17Block <- CopyBytes(WorkBlock[1], CarryLen)
      WorkBlock <- CopyBytes(WorkBlock[1+CarryLen], ByteInLen-CarryLen)
    DiffLen <- 2*WordInLen+CarryLen-ByteInLen
    While (DiffLen > 0) do
      WorkBlock <- ConcatBlocks(WorkBlock, 0x00h)
      DiffLen <- DiffLen-1
    BlockIndex <- 1
    For ChunkNo = 1 to WordInLen do
      ChunkData <- 0x100h*WorkBlock[BlockIndex]+WorkBlock[BlockIndex+1]
      BlockIndex <- BlockIndex+2
      If ((CryptType = CK_DECRYPT) and LastBitIsSet(Bit17Block)) then
        ChunkData <- 0x10000h+ChunkData
      ChunkData <- CRYPT_RSA(ChunkData, RSAExp, RSAMod)
      If ((CryptType = CK_ENCRYPT) then
        Bit17Block <- BitShiftLeft(Bit17Block, 1)
        If (ChunkData >= 0x10000h) then
        Bit17Block <- BitShiftRight(Bit17Block, 1)
      ChunkData <- Last16Bits(ChunkData)
      DataOut <- ConcatBlocks(DataOut, ChunkData)
    If (CryptType = CK_ENCRYPT) then
      DataOut <- ConcatBlocks(Bit17Block, DataOut)
      DataOut <- CRYPT_STREAM(DataOut, FWD, E467F6A3)

That is all there is to it. Simple, no? It should present no great difficulty to translate the above into usable code. Many optimisations are possible when coding the function, particularly in assembler, where integer arithmetic is very amenable to calculating sizes, and most of the elementary functions such as "LastBitIsSet", "CopyBytes", "ConcatBlocks", etc. can be readily and concisely prepared.

4. The Taming of the CRC

We are now at the stage where applying the CRYPT_CRYPKEY function will yield some useful results. There is, however, one further aspect to the CrypKey data blocks, i.e. Site Codes and Keys, data in the licence files, etc., that needs to be looked at. CrypKey always decides whether a particular data block is valid on the basis of a 16-bit CRC (cyclical redundancy check) value. The CRC value is always added at the end of a block of data prior to encryption. Verification normally happens after decryption by computing the CRC for the data block, including the final pair of bytes. If the computed CRC is zero, the block is considered to be genuine, and processing continues. If the computed CRC is non-zero, CrypKey rejects the block. The reason for this authentication working as it does is that the CRC function has the property that if, while calculating a block's CRC value, the next 16-bit chunk of the block is equal in value to the CRC of the block up to the current position, then the very next calculation cycle will yield a CRC value of zero.

This will perhaps become somewhat clearer when looking at a pseudo-code rendition of the CRC function :

    ByteInLen <- ByteCount(DataIn)
    Result <- 0xFFFFh
    For ByteNo = 1 to ByteInLen do
      TempWord <- 0x100h*ReverseByteBits(DataIn[ByteNo])
      Result <- Result XOR TempWord
      For ResultBitNo = 1 to 8 do
        Result <- 2*Result
        If (Result >= 0x10000h) then
          Result <- Result XOR 0x18005h
    TempHiByte <- Truncate(Result/0x100h)
    TempLoByte <- Result MOD 0x100h
    TempHiByte <- ReverseByteBits(TempHiByte)
    TempLoByte <- ReverseByteBits(TempLoByte)
    Result <- 0x100h*TempLoByte+TempHiByte

The "ReverseByteBits" function in the above code returns a byte that is the mirror image of the input byte, i.e. the input byte in reverse bit order. For example, a call to ReverseByteBits with an input byte of 0x5Dh will return 0xBAh since 0x5Dh is 01011101 in binary, which in reverse order is 10111010, or 0xBAh. Again, it is possible to make significant optimisations when translating the above pseudo-code into a usable function. However, the two most important aspects of the above code are that the result can never be greater than 16 bits, i.e. 0xFFFFh, and that the result is returned in native x86 byte order.

To illustrate in more concrete terms the earlier points about CrypKey's validation of data blocks, the following step-by-step example shows how to go about preparing a block for encryption :

  Given:    DataIn  = 42756D
  Required: Prepare as valid CrypKey data block with CRC
  Step(1)   CRCValue <- 0xFFFFh
  Step(2.1) RevWord <- 0x100h*ReverseByteBits(0x42h) = 0x4200h
            CRCValue <- CRCValue XOR 0x4200h = 0xBDFFh
              CRCValue <- 2*CRCValue = 0x17BFEh, XOR 0x18005h = 0xFBFBh
              CRCValue <- 2*CRCValue = 0x1F7F6h, XOR 0x18005h = 0x77F3h
              CRCValue <- 2*CRCValue = 0xEFE6h
              CRCValue <- 2*CRCValue = 0x1DFCCh, XOR 0x18005h = 0x5FC9h
              CRCValue <- 2*CRCValue = 0xBF92h
              CRCValue <- 2*CRCValue = 0x17F24h, XOR 0x18005h = 0xFF21h
              CRCValue <- 2*CRCValue = 0x1FE42h, XOR 0x18005h = 0x7E47h
              CRCValue <- 2*CRCValue = 0xFC8Eh
  Step(2.2) RevWord <- 0x100h*ReverseByteBits(0x75h) = 0xAE00h
            CRCValue <- CRCValue XOR 0xAE00h = 0x528Eh
              CRCValue <- 2*CRCValue = 0xA51Ch
              CRCValue <- 2*CRCValue = 0x14A38h, XOR 0x18005h = 0xCA3Dh
              CRCValue <- 2*CRCValue = 0x1947Ah, XOR 0x18005h = 0x147Fh
              CRCValue <- 2*CRCValue = 0x28FEh
              CRCValue <- 2*CRCValue = 0x51FCh
              CRCValue <- 2*CRCValue = 0xA3F8h
              CRCValue <- 2*CRCValue = 0x147F0h, XOR 0x18005h = 0xC7F5h
              CRCValue <- 2*CRCValue = 0x18FEAh, XOR 0x18005h = 0x0FEFh
  Step(2.3) RevWord <- 0x100h*ReverseByteBits(0x6Dh) = 0xB600h
            CRCValue <- CRCValue XOR 0xB600h = 0xB9EFh
              CRCValue <- 2*CRCValue = 0x173DEh, XOR 0x18005h = 0xF3DBh
              CRCValue <- 2*CRCValue = 0x1E7B6h, XOR 0x18005h = 0x67B3h
              CRCValue <- 2*CRCValue = 0xCF66h
              CRCValue <- 2*CRCValue = 0x19ECCh, XOR 0x18005h = 0x1EC9h
              CRCValue <- 2*CRCValue = 0x3D92h
              CRCValue <- 2*CRCValue = 0x7B24h
              CRCValue <- 2*CRCValue = 0xF648h
              CRCValue <- 2*CRCValue = 0x1EC90h, XOR 0x18005h = 0x6C95h
  Step(3)   TempHiByte <- ReverseByteBits(0x6Ch) = 0x36h
            TempLoByte <- ReverseByteBits(0x95h) = 0xA9h
            CRCValue <- 0xA936h
  Step(4)   TempWord <- SwapBytes(CRCValue) = 0x36A9h
            DataOut <- ConcatBlocks(DataIn, TempWord) = 42756D36A9

To confirm that the "DataOut" block is indeed an acceptable data block, the CRC of the block will now be calculated. We can continue from Step(2.3) above because we already know "CRCValue" for the first three bytes (0x6C95h) :

  Step(2.4) RevWord <- 0x100h*ReverseByteBits(0x36h) = 0x6C00h
            CRCValue <- CRCValue XOR 0x6C00h = 0x0095h
              CRCValue <- 2*CRCValue, eight times with no XOR 0x18005h = 0x9500h
              (all eight high bits are clear to begin with, thus no XOR 0x18005h)
  Step(2.5) RevWord <- 0x100h*ReverseByteBits(0xA9h) = 0x9500h
            CRCValue <- CRCValue XOR 0x9500h = 0x0000h
              CRCValue <- 2*CRCValue, eight times with no XOR 0x18005h = 0x0000h
              (all eight high bits are clear to begin with, thus no XOR 0x18005h)
  Step(3)   TempHiByte <- ReverseByteBits(0x00h) = 0x00h
            TempLoByte <- ReverseByteBits(0x00h) = 0x00h
            CRCValue <- 0x0000h

Apart from illustrating the operation of CrypKey's CRC function and providing some reference data for the reader who wishes to code his or her own CrypKey suite, the above example also clearly illustrates why appending the CRC of a data block onto the end of the block causes the CRC of the whole lot to assume a value of zero.

5. A Site to Behold

With the CRYPT_CRYPKEY and CRC16_CRYPKEY functions, the arsenal of tools required for further dissecting CrypKey is completed. Such further dissection will provide an understanding of several other aspects and weaknesses in CrypKey. This section will consider the relationship and meaning of the Site Code and the Site Key which corresponds to it. However, before diving headlong into the analysis, it is worth expanding a little upon an aspect that was briefly touched on in the introduction and, indirectly, in the earlier section describing the CRYPT_STREAM function. The introduction stated that CrypKey exchanges data blocks by means of ASCIIz strings and the CRYPT_STREAM discussion mentioned that the length of the input data block was implicit in the data block itself. From a notational point of view, it is not at all difficult to make the mistake of thinking that the only difference between D36073F202730AB760 and "D360 73F2 0273 0AB7 60" is that the latter has some spaces thrown in as separators to enhance legibility. But the difference between the two is far greater. The quotation marks around the second sample clearly indicates it to be an ordinary string of characters, i.e. "D" followed by "3" followed by "6", etc., whereas the first sample represents a block of binary data, i.e. a byte with the value 0xD3h followed by one with the value 0x60h, etc.

ASCIIz (null-terminated) strings make for easy reading, but little else that would recommend their use in an essentially binary environment such as CrypKey's licence management. Aside from the considerable processing overhead in converting strings to binary data blocks and back again, the cracker's job is also simplified because the size of the block and its content (though not its meaning) are immediately and plainly apparent. The whole point to software piracy protection is not so much to prevent unauthorised use and distribution as it is to limit these, and a cardinal rule in this endeavour is to make it as cumbersome as possible for the cracker to circumvent the security measures that are implemented. This is why a product such as Armadillo, for which no generic attack short of brute force exists, is notably superior to CrypKey, not to mention decidedly cheaper. It is important to realise that Armadillo is not more difficult to break than CrypKey, but it takes much more time and, far more significantly, breaking one Armadillo-protected program is most certainly not an "Open, Sesame!" to other Armadilloed kith and kin. The same does not apply to CrypKey since the structure and meaning, as well as the relationship between the Site Code and Site Key, are identical in each instance of the CrypKey affliction.

5.1 The Site Code

So what's in a Site Code? The byte sequence used in the opening paragraph of this section as an example to differentiate between a string and binary data is a Site Code from CKI. It is as good a victim as any other for the purpose of elucidating the Site Code. The first step is to feed the Site Code to CRYPT_CRYPKEY with the correct parameters :

  CRYPT_CRYPKEY(D36073F202730AB760, CK_DECRYPT, 1069, 123107)
  = 903600206278FDC7

It is left as an exercise for the reader to verify that the original Site Code is obtained from (note the parameter changes) :

  CRYPT_CRYPKEY(903600206278FDC7, CK_ENCRYPT, 229, 123107)

The decoded block doesn't look any more fruitful than the original except that it is shorter by one byte. But if we stick it into CRC16_CRYPKEY we obtain a result that should not surprise anyone in view of the earlier discussions on the CRC :

  = 0x0000h

This reveals that the final two bytes of the block, i.e. FDC7, constitute the CRC of the first six bytes. The decrypted Site Code block can be separated into five distinct fields as shown below :

 +----+  +----+  +------+  +------+  +------+
 | 90 |  | 36 |  | 0020 |  | 6278 |  | FDC7 |
 +--+-+  +--+-+  +---+--+  +---+--+  +---+--+ 
    |       |        |         |         |
    |       |        |         |         +- 5. Site Code CRC
    |       |        |         |
    |       |        |         +----------- 4. Program Location Info
    |       |        |
    |       |        +--------------------- 3. Company & Product ID Nos.
    |       |
    |       +------------------------------ 2. CrypKey Version No.
    +-------------------------------------- 1. Instance ID & Flag

A short description of each field follows.

5.1.1 Instance ID & Flag

The lower seven bits of this byte make up the instance identifier of the protected program. This ID value seems to be generated in a random manner when the program is run for the first time and is saved as part of the ".ent" licence file so as to preserve the same Site Code between runs. It serves mainly to match the Site Code with the Site Key, where it also puts in an appearance (see below). The uppermost bit of this byte is a flag bit that indicates whether or not the software already has a valid licence and, if so, whether the licence can be upgraded. Thus, in the given case, the value of 0x90h means that the instance ID is 16 (0x10h = 0x90h AND 0x7Fh), that CKI is properly licensed (0x90h AND 0x80h != 0) and that the existing licence can be upgraded. This value changes when a valid Site Key is entered, and thus causes the Site Code and its CRC to change.

5.1.2 CrypKey Version No.

This byte merely reflects the version number of CrypKey that protects the program and that generated the Site Code. In this case the version number is 5.4 and the value of the byte is thus 10*5.4 = 54 = 0x36h.

5.1.3 Company & Product ID Nos.

This 16-bit field contains a company ID and the product number the company uses to identify the product. The lower 10 bits constitute the company ID, while the high six bits are the product number. Oddly enough, this field is stored in native x86 byte order, i.e. little endian, so that the value in the present case is 0x2000h, which yields a company ID of 0 (0x2000h AND 0x3FFh = 0) and a product number of 8 (Truncate(0x2000h/0x400h) = 8). It should not come as any great surprise that the chaps at Kenonic have elected to be company number zero. More than that, it seems fitting and perhaps even prophetic. We will encounter this 16-bit field again in the next section where the Master and User Keys are discussed.

5.1.4 Program Location Info

This 16-bit field is the most mysterious card in the deck. I have not gone to any great trouble to discover how it is generated since this would be a mostly useless exercise, given that we are able make any Site Key we choose. It is sufficient to know that this field ties the protected program to a specific location on the hard disk drive. On Win9x/ME FAT drives, this field's value is very likely related to the number of the disk allocation units (clusters) that contain the licence files, while on NT-family systems it is most probably related to the disk's volume ID and the path to the licence files. This field also appears in the Site Key, albeit in a somewhat altered guise (see below), which ties the Site Key to the same location as the Site Code. It is also useful to note that this value is different for each instance of a protected program, i.e. if you have CKI installed in three different locations, the value of this field is different for each copy thereof. Moreover, this field also exhibits a time-dependency in that clearing out the licence files results in a different value being generated, despite the licence files not being moved.

5.1.5 Site Code CRC

As mentioned earlier, this 16-bit field merely ensures that the Site Code's CRC is zero, which marks it as a valid CrypKey data block, and thus as a valid Site Code. Note that CRC16_CRYPKEY(903600206278) = 0xC7FDh, and appending this CRC in reverse byte order yields the original decrypted Site Code.

5.2 The Site Key

The Site Key "D337 DC3A 2303 9D3E 65AC 9B35 9E" is one of many that will work with the example Site Code used above. I will use this Site Key to explain the meaning and structure of a Site Key in a similar way to that of the Site Code. Again, the first step is to decrypt it with the correct parameters :

  CRYPT_CRYPKEY(D337DC3A23039D3E65AC9B359E, CK_DECRYPT, 229, 123107)
  = 90058FA5785634120C001D41

It is crucially important to note the difference in the RSA decryption parameters between the Site Code and Site Key - they use complementary RSA keys.

Here the reader is also encouraged to verify that :

  CRYPT_CRYPKEY(90058FA5785634120C001D41, CK_ENCRYPT, 1069, 123107)
  = D337DC3A23039D3E65AC9B359E

and that

  = 0x0000h

The decrypted Site Key consists of seven fields, for each of which a more detailed description follows :

 +----+  +----+  +------+  +------+  +------+  +------+  +------+
 | 90 |  | 05 |  | 8FA5 |  | 7856 |  | 3412 |  | 0C00 |  | 1D41 |
 +--+-+  +--+-+  +---+--+  +---+--+  +---+--+  +---+--+  +---+--+
    |       |        |         |         |         |         |
    |       |        |         |         |         |         +- 7. Site Key CRC
    |       |        |         |         |         |
    |       |        |         |         |         +----------- 6. Limit Count & Type
    |       |        |         |         |
    |       |        |         |         +--------------------- 5. User Data (Options)
    |       |        |         |
    |       |        |         +------------------------------- 4. User Data (Level)
    |       |        |
    |       |        +----------------------------------------- 3. Location Info Hash
    |       |
    |       +-------------------------------------------------- 2. Licence Count
    +---------------------------------------------------------- 1. Instance ID & Flag

5.2.1 Instance ID & Flag

The lower seven bits of this byte are identical to those of the Site Code which it was generated for. This is always the case and ensures that the Site Key matches the Site Code. The most significant bit is also a flag bit, but its significance is subtly different to that of the Site Code. If it is set, it tells CrypKey that the licence data in the Site Key must be added to any existing licence, whereas if the bit is clear, the new licence data must replace the old, if any. For example, the program may be licensed for 30 days with 15 days remaining. The new key might say that it will allow the program to be used for 12 days. If the bit in question is set, entering the Site Key will extend the allowed usage period by 12 days from 15 to 27, while if the bit is clear, the allowed usage period will be the 12 days specified in the Site Key.

5.2.2 Licence Count

This field is a signed (two's complement) eight-bit integer containing the number of licences that the key is valid for. Its range is from -128 to +127 inclusive. A negative value signifies a network licence with a licence count that is equal to the absolute value of the field. For example, a value of 0xFBh means five network licences because -5, represented as an eight-bit signed integer, is 0xFBh. If the value is positive, an ordinary licence is indicated. In the latter case, it seems pointless to have a licence count that is more than one since CrypKey doesn't mind starting the same program more than once even when the licence count is limited to one only. On the other hand, a network licence count of five will allow a maximum of five instances of the program to operate at any given time. The reader should be wary of using the values -128 and zero (0x80h and 0x00h) in this field because some CrypKey protected programs react strangely to these end-point values.

5.2.3 Location Info Hash

This 16-bit field is the Site Code's Location Info field disguised in a weak hash. Its construction is best illustrated by means of an example. The Location Info in the original Site Code consists of the bytes 0x62h and 0x78h. If we add 0x2Dh to each of these separately, we obtain the bytes 0x8Fh and 0xA5h, respectively, which are the Site Key's Location Info Hash bytes. For want of a more descriptive name, I will call the additive byte (i.e. 0x2Dh) the "User Product ID". Its origin will be completely explained in the next section on the Master and User Keys. For now, it is enough to know that (a) it is derived from the User Key, (b) it always takes the same value for a given product, irrespective of the computer and installation path, and (c) the sum must be done one byte at a time, ignoring any carry that may arise along the way. This means that in the case of generating Site Keys for CKI, the value 0x2Dh must always be used, and that doing a 16-bit add of 0x2D2Dh to the Site Code's Location Info field, rather than adding 0x2Dh to each byte, can result in an invalid Site Key. To illustrate this last point, consider what would happen if the Location Info was E4D9. Remembering the x86's byte ordering, a 16-bit add would yield 0xD9E4h+0x2D2Dh = 0x10711h. Ignoring the carry (17th bit), gives us a hash value of 0x0711h, which would be stored as 1107 in the unencrypted Site Key. However, the correct hash value to use here is 1106, since 0xE4h+0x2Dh = 0x111h ->0x11h (carry ignored) and 0xD9h+0x2Dh = 0x106h -> 0x06h (carry ignored).

5.2.4 User Data (Level)

CrypKey's Site Key Generator (SKW) defines two 16-bit data fields that form a part of the Site Key. These fields go under the names "Level" and "Options" (see also next heading), but these are merely a convenient way of subdividing a 32-bit user data area. The protected program can retrieve the values of these fields through various means, e.g. command line switches (DOS), environment variables or calls to CrypKey functions. The important point to note here is that the content of these fields is very much program-specific and entirely at the discretion of the program originator, i.e. CrypKey itself doesn't care a jot what's in them. That said, the default 16-bit Level/Options split of this 32-bit field is nonetheless frequently retained in a protected program to allow version control and to enable or disable certain parts of the program, depending on the licence.

When the default split is used, the first 16 bits normally specify some form of usage level, for example the version number of the program, or a derivative thereof, that the licence is valid for. The programmer is left to decide what action to take if you're trying to run version 4.1, say, of his or her software with a licence that is valid for versions 3.7 and below, because, as said, CrypKey itself doesn't care about such things.

5.2.5 User Data (Options)

This 16-bit field comprises the second half of the 32-bit user data area (see also previous topic), which can be used in any way a programmer sees fit. In the case where CrypKey's default Level/Options split is retained, this 16-bit field usually is a collection of bit flags, each of which enables or disables a specific program facility, e.g. "Save As...", "Print", etc. In other words, it normally tells the program which options the user is permitted to make use of. Unfortunately, owing to the discretionary nature of the 32-bit user data field, there exists no generic fail-safe method (short of reverse engineering) by which the meaning and structure of this field can be ascertained for every CrypKey protected program. However, it is often possible to make some intelligent trial-and-error guesses on the basis of assuming that the default split, i.e. a 16-bit Level and 16-bit Options field, has been used, and if the program in question has been protected with CKI rather than the CrypKey SDK, the Level and Options values are rarely of any consequence.

5.2.6 Limit Count & Type

This 16-bit field contains the limit count and limit type of the licence. A value of zero in this field normally indicates an unlimited licence. The lower 15 bits specify the limit count, which can range from 0 to 32767 (0x7FFFh), while the most significant bit is used to distinguish between a licence that is runs-limited and one that is days-limited. If the bit is clear, the limit specifies the number of times that the program may run before the licence expires. If the bit is set, the limit specifies the number of days that the program can be used for after entering the Site Key. It is important to note that the byte order of this field is native x86 format. The example value 0C00 above represents 0x000Ch, i.e. the licence is limited to 12 runs.

5.2.7 Site Key CRC

As with the Site Code, this 16-bit field merely ensures that the Site Key's CRC is zero, thus preventing CrypKey from having a frothy about invalid data blocks. At the risk of becoming tedious, CRC16_CRYPKEY(90058FA5785634120C00) = 0x411Dh, which is the value of this field in reverse byte order.

From the preceding dissections of the Site Code and Site Key, the standard recipe for producing Site Keys for any (valid) Site Code emerges almost naturally :

  (1) CodeBlock <- CRYPT_CRYPKEY(SiteCode, CK_DECRYPT, 1069, 123107)
      (validity check: CRC16_CRYPKEY(CodeBlock) ?= 0x0000h)
  (2) KeyBlock[1] <- CodeBlock[1] AND 0x7Fh (unsigned byte)
      (KeyBlock[1] <- KeyBlock[1] OR 0x80h if licence upgrade)
  (3) KeyBlock[2] <- LicenceCount (signed byte)
      (LicenceCount < 0 if network licence)
  (4) KeyBlock[3] <- CodeBlock[5]+UserProductID (unsigned byte)
      KeyBlock[4] <- CodeBlock[6]+UserProductID (unsigned byte)
  (5) KeyBlock[5..8] <- UserData (32-bit / 16-bit Level + 16-bit Options)
  (6) KeyBlock[9..10] <- LicenceLimit (16-bit)
      (0 = unlimited; MSB set for days, clear for runs limit)
  (7) KeyBlock[11..12] <- CRC16_CRYPKEY(KeyBlock[1..10]) (16-bit)
      (Reverse byte order)
  (8) SiteKey <- CRYPT_CRYPKEY(KeyBlock, CK_ENCRYPT, 1069, 123107)
      (SiteKey is 13 bytes long)

The only difficulties in this recipe lie in selecting the correct 32-bit UserData and in obtaining the correct UserProductID. As indicated earlier, some trial-and-error and/or reverse engineering may be necessary before the appropriate UserData is found, and, in this context, it may prove profitable to assume that this 32-bit field consists of 16 bits of Level data, followed by 16 bits of Options data. The "UserProductID" value and its origin will be described in the following section.

6. Mastering the User Key, and Vice-versa

When using CKI to protect a given program, one of the steps encountered during the protection process requires specifying a "Master key" and a "User key". The keys are grouped under the heading "Program keys" under the "CrypKey" section of CKI's user interface. The documentation that accompanies CKI reveals that the User Key is "created from the CrypKey password you specify", and the Master Key is "created from the the file name you give to CrypKey and other pertinent information". Yes, this means that you have to supply Kenonic with your CrypKey password before they are able to provide you with a User Key, unless, that is, you choose to make your own with the information provided herein. A password is supposed to be a private thing, and having to divulge it, even in the face of a confidentiality promise, is a major black mark against CrypKey in my estimation. In addition, it will shortly become evident that the password is used in an awfully insecure way to generate a User Key, by which I mean that recovering a workable password from the User Key is by no means rocket science.

The password also appears in the product configuration of SKW. Later on, it will be demonstrated how SKW can be licensed so that it generates Site Keys for someone else's product. The Master and User Keys are not as such visible to the ordinary user of a protected program. CrypKey uses them in an explicit call to a function by the name of "init_crypkey()", which expects three ASCIIz pointer and two dword arguments (in that order). The first ASCIIz pointer specifies the name plus path of the protected program; the second and third parameters reference the Master and User Keys, respectively. Although it is possible to obtain the entry point RVA of init_crypkey() for Win9x/ME from a disassembly or dump of Cryp95e.dll, and to use this to get the Master and User Keys via a debugger, this is not necessary in the case of CKI because both keys are stored in plain text format in the data section of CKI, despite the stealth functionality of the program. I will use CKI's Master and User Keys as examples to explain their structure and content.

  CKI Master Key = EC99 55C4 1D10 E912 00F4 D5A5 4C5E 0AA0 FB1B 0AA0 ED0D
  CKI User Key = D656 8110 02FE 78

It is a safe bet that the keys are encrypted data and this is indeed the case :

  CRYPT_CRYPKEY(EC9955C41D...1B0AA0ED0D, CK_DECRYPT, 809, 130771)
  = 00000020434B492E455845000000000000003837
  CRYPT_CRYPKEY(D656811002FE78, CK_DECRYPT, 229, 123107)
  = 090809080B00

Not surprisingly, the decrypted Master Key has the property :

  = 0x0000h

The reader may perhaps have noticed that the third and fourth bytes of the decoded Master Key are our old friend, the Company & Product ID, which also appears in the decrypted Site Code (see previous section). The byte sequence 434B492E455845 does not seem to make a great deal of sense until it is converted to ASCII, i.e. 0x43h -> "C", 0x4Bh -> "K", etc., and the string "CKI.EXE" emerges, which clearly is the name of the file or module CrypKey is supposed to protect. This string can have a maximum of 12 characters (eight name, one dot and three extension), but fewer can be used by null-padding the string out to a total length of 12 characters. It is also worth noting that the string always seems to contain only uppercase letters, but this may not be a strict requirement. The meaning of the first two bytes and that of the pair following the name string is unclear, but I have yet to find any examples where these have non-zero values. In keeping with CrypKey's custom, the last two bytes are the CRC of the Master Key.

Based on the above, we will now make a Master Key of our own. Suppose our product is called "ANNOY.ME", that it is product no. 3 and that we have chosen our company ID no. to be 666. The 16-bit Company & Product ID thus becomes 3*1024+666 = 3738 = 0x0E9Ah, and "ANNOY.ME" becomes the byte sequence 414E4E4F592E4D4500000000 when null-padded to 12 characters. Therefore, the unencrypted Master Key at this point is 00009A0E414E4E4F592E4D45000000000000, where two leading and two trailing bytes with values of zero have been added. Since CRC16_CRYPKEY(00009A0E41...0000000000) = 0xBBE6h, the Master Key becomes 00009A0E414E4E4F592E4D45000000000000E6BB before encryption. Finally,

  CRYPT_CRYPKEY(00009A0E414E...00000000E6BB, CK_ENCRYPT, 2089, 130771)
  = C0B757C431400545B9B202A0EE3D1E8D3A281E8DBF9D

.....and our Master Key is "C0B7 57C4 3140 0545 B9B2 02A0 EE3D 1E8D 3A28 1E8D BF9D".

The User Key is an entirely different can of worms. The example from CKI produced the block 090809080B00 upon decryption, and this sequence of bytes is derived from the password. CKI's proper password is unknown to me, but that makes little or no difference. For now, it is sufficient to know that it consists of five uppercase characters and that "HANDY" (among many others) will work just fine for a password to demonstrate how the User Key is generated. Quite simply, it entails taking one password character at a time in sequence and truncating the result of dividing its ASCII byte value by eight. Thus,

  "H" -> 0x48h, Truncate(0x48h/8) = 0x09h
  "A" -> 0x41h, Truncate(0x41h/8) = 0x08h
  "N" -> 0x4Eh, Truncate(0x4Eh/8) = 0x09h
  "D" -> 0x44h, Truncate(0x44h/8) = 0x08h
  "Y" -> 0x59h, Truncate(0x59h/8) = 0x0Bh
  i.e. "HANDY" -> 090809080B

The reader may protest that the decrypted User Key actually consists of six bytes, but notice that the last one has a value of zero, which is a relic from encrypting an odd number of bytes. Strangely, the User Key is different to all other CrypKey data blocks in that it does not include a CRC. From the above example, it should be patently obvious how weakly the password is used - the lower three bits of each password character are simply discarded. For any given User Key, it is a trifling matter to find many different passwords that all produce the same User Key. This weakness is aggravated by the fact that the password is not case-sensitive and is limited to 12 characters. So apart from having to disclose your carefully chosen password, it is used in a manner that adds hardly anything in the way of security to your product. This is yet another instance of the severely dodgy way in which CrypKey has been implemented, for would not the end user be much more comfortable if CKI and/or SKW allowed him or her to obtain the User Key without having to tell Kenonic the password?.

The required functionality for doing so in any case already exists in CrypKey because, using data from the previous example, the original User Key can easily be recovered :

  CRYPT_CRYPKEY(090809080B, CK_ENCRYPT, 1069, 123107)
  = D656811002FE78.

One additional aspect - probably the most important one at that - of the User Key is that it permits us to find that elusive "UserProductID" value required to make a Site Key. This value is simply the sum of the decrypted User Key bytes. In the previous section, the "UserProductID" used for making the CKI Site Key had a value of 0x2Dh, and 0x09h+0x08h+0x09h+0x08h+0x0Bh = 0x2Dh, which is not a coincidence.

It is understandable that Kenonic may wish to retain their position as sole agent for dispensing Master Keys - it allows them to keep track of all of your company's software that is protected by CrypKey. Giving them the password summarily removes what could be the only obstacle to their generating Site Keys for your product(s), even if it is only a less-than-worrisome eight-bit obstacle. Of course, they will promise that they would never do such a thing, but whether they actually do or not is not at issue here; it is that they are in a favourable position to do so in the first place without having informed you accordingly, and this flies in the face of what software protection is basically all about: you, and only you, as the author/distributor/owner should be able to provide licences for its use.

A note on licensing SKW: CrypKey's Site Key generator has the "UserProductID" with a value of 0x48h (the password is "MUNCHKIN"). To enable SKW so that it generates keys for the products of a company whose ID is X, set the 16-bit Level field to X, the 16-bit Options field to 0x8004h ("Master Copy" & "Authorize All Products") and limit the licence to zero days (no, I don't understand this either)! For example, if the company ID is 666 = 0x029Ah, the portion of the unencrypted Site Key making up the 32-bit UserData followed by the 16-bit Limit Count & Type field will be the block 9A0204800080 (Level = 0x029Ah, Options = 0x8004h and Limit = 0x8000h). SKW will, if licensed with these parameters, refuse to generate any Site Keys for Site Codes that indicate a company ID that differs from 666. The central point to note here is that the Level field of SKW's Site Key is the company ID SKW will generate Site Keys for. Thus, if you license SKW for a company ID of zero and configure an application in SKW whose ID is 8 (refer to the Site Code discussion) and that uses the password "HANDY", you can start churning out Site Keys for CKI.

7. Some Files in the Soup

The present section will provide a brief overview of the CrypKey licence files and their content. Although this information is peripheral to the main thrust of this article, it is presented for the sake of interest and completeness. In addition, some CrypKey protected programs allow the user to "kill" an existing licence, i.e. to remove the licence information, whereupon CrypKey spews out a confirmation code that can be used as proof that the licence was deleted. The structure and content of these confirmation codes will also be elaborated on.

As mentioned elsewhere, there are normally four licence files. They have the same name as the file or module specified in the Master Key and normally live together with the protected file in the same directory but their extensions differ from it, being "41s", "ent", "rst" and "key". The licence files are marked with the system and hidden attributes.

The ".41s" file seems always to be zero bytes in size and hence does not house any information. It probably serves purely as a marker and possibly as a file system time stamp.

The ".key" file contains either "DO_NOT_DISTURB" or the most recent valid Site Key in plain text but without the usual separating spaces. The structure and meaning of the Site Key was explored in an earlier section.

Far more interesting is the ".ent" file, which can contain either "DO_NOT_DISTURB" or a plain text string representing a CrypKey data block. An example, taken from "CKI.ent", is "D6A6A60D630FEA" :

  CRYPT_CRYPKEY(D6A6A60D630FEA, CK_DECRYPT, 229, 123107)
  = 0200F56D8721


  = 0x0000h that the final two bytes of the decrypted ".ent" file are, once again, the CRC. The first byte, 0x02h, is the Instance ID, which occurs in the decrypted Site Code as well (refer to the earlier discussion thereof). Note, however, that unlike in the case of the decrypted Site Code, this byte's uppermost bit is always clear in the ".ent" data block. The second byte, 0x00h, is zero in every case that I have seen and conceivably could be padding or the result of zero-extending the Instance ID to 16 bits. The next two bytes are a little mysterious. At first, it appears as though they comprise a 16-bit field with the same value as the Program Location Info field in the decrypted Site Code, but killing the licence (if the program has this CrypKey feature enabled) changes both of these values in the ".ent" block and in the Site Code so that they are no longer equal. A relationship between the two is almost certain, but its nature is obscure.

The ".rst" file contains either "DO_NOT_DISTURB" or encrypted licence restriction information, also as a plain text string. An example of such a data block in this file is "C7E872324058D64F95C1F117670009" :

  CRYPT_CRYPKEY(C7E872324058D64F95C1F117670009, CK_DECRYPT, 229, 123107)
  = 27BA06000100C00D1BBC6EAA1161


  = 0x0000h

whence the last two bytes are - surprise, surprise - the block's CRC. The decoded ".rst" data block can be broken up into six fields, as shown below :

 +------+  +------+  +------+  +----------+  +------+  +------+
 | 27BA |  | 0600 |  | 0100 |  | C00D1BBC |  | 6EAA |  | 1161 |
 +---+--+  +---+--+  +---+--+  +-----+----+  +---+--+  +---+--+
     |         |         |           |           |         |
     |         |         |           |           |         +- 6. Restriction Info CRC
     |         |         |           |           |
     |         |         |           |           +----------- 5. Unknown
     |         |         |           |
     |         |         |           +----------------------- 4. Licence Date
     |         |         |
     |         |         +----------------------------------- 3. Remaining Licences
     |         |
     |         +--------------------------------------------- 2. Usage/Day Count
     +------------------------------------------------------- 1. Last Use Time Stamp

The "Last Use Time Stamp" field is a 16-bit time counter that increments in units of one second. It is updated during the protected program's initialisation stage to a value that is the current time of day reckoned in seconds since midnight and taken MOD 65536 (= 0x10000h). There are 86400 (= 0x15180h) seconds in a day, but the counter has only 16 bits and so it rolls over to zero at 18:12:16. The sample value of 27BA translates to 0xBA27h (native x86) = 47655 = 13:14:15. Why, one may well ask, didn't they rather opt for a resolution of two seconds? Only the chaps at Kenonic know the answer. Be that as it may, the primary purpose of this field is to foil attempts at fudging the system time to extend the lifespan of a licence that is days-limited.

The "Usage/Day Count" field is also a 16-bit counter that is used to keep track of the number of times the program has started if the licence is runs-limited, or the number of days since the Site Key was entered if the licence is days-limited, or, in the case of an unlimited licence, nothing at all. CrypKey reports the licence as expired when this counter reaches the limit value specified in the Site Key if the licence is limited. The example value of 0600 is 0x0006h = 6, and could thus mean either that the program has already executed six times since the Site Key was entered or that the Site Key was entered six days before the program was last run. The meaning of this field thus depends on the type of limitation that the Site Key specifies. Like the previous one, this field is also updated every time when the protected program first starts up.

The "Remaining Licences" field is yet another 16-bit counter, the purpose of which is to keep a tally of the number of licences and their type (network or ordinary) that the installation is good for. To illustrate the principle involved here, let us suppose that the Site Key specifies a licence count of 25 and that we have been generous by giving 14 of them away to our buddies using CrypKey's licence transfer functions (which are an enormous pain in the arse to use, just by the way). This counter will then contain the value 11. The sample value of 0100 is 0x0001h, i.e. there is a single licence remaining. Note that this counter field is akin to the Site Key's Licence Count field, except that it is 16 bits long, rather than eight.

It actually contains a signed 16-bit integer that, if negative, indicates that the remaining licences are of the network kind.

The "Licence Date" field is an unsigned 32-bit integer record of the date and time when the Site Key was entered. More precisely, it contains the number of seconds that have elapsed between 00:00:00 on 01 January 1900 and the moment when CrypKey decided that the Site Key was cool. This field does not suffer any changes during its life, unless an existing licence is upgraded, extended or killed. The example value of C00D1BBC is 0xBC1B0DC0h = 3155889600, which corresponds to 12:00:00 on 01 January 2000. (See the appendix for a method of converting the CrypKey date/time values to a more recognisable form, and vice-versa.) It is worth noting that the value CrypKey stores in this field is in error by one day, most likely because the calculations ignore the fact that the year 1900 was not actually a leap year. In the example therefore, Kenonic would quite wrongly insist that the date/time value refers to 12:00:00 on 31 December 1999.

The "Unknown" field is wholly obscure because it behaves differently on NT-family systems, where it changes between runs, versus Win9x/ME, where it seems to remain static. Indications are that this is a single 16-bit field, rather than a pair of eight-bit fields each. It is updated each time the protected program is run, and it may be a counter of some type, in which case the observed changes on NT-family systems happen very rapidly.

Lastly, the "Restriction Info CRC" field fulfils the same function as every other CRC field in the different CrypKey data blocks does: it ensures a CRC of zero for the entire block.

One of the configuration options in CKI specifies whether the end user can remove an existing licence for a protected program via the CrypKey interface (as opposed to simply deleting the licence files or the NT master list). When this option is enabled and a user decides that he or she wants to kill the licence, CrypKey emits a confirmation code. The string "C435 B0BE 64CE 053C 07F9 9ECD 35" is an example code of this type and will be used to elucidate the general structure thereof :

  CRYPT_CRYPKEY(C435B0BE64CE053C07F99ECD35, CK_DECRYPT, 1069, 123107)
  = 02A536FB00203AB0A3BF6AD3

and, once again,

  = 0x0000h

The decrypted confirmation code can be broken down into six fields :

 +------+  +----+  +----+  +------+  +----------+  +------+
 | 02A5 |  | 36 |  | FB |  | 0020 |  | 3AB0A3BF |  | 6AD3 |
 +---+--+  +--+-+  +--+-+  +---+--+  +-----+----+  +---+--+
     |        |       |        |           |           |
     |        |       |        |           |           +- 6. Confirmation Code CRC
     |        |       |        |           |
     |        |       |        |           +------------- 5. Licence Kill Date
     |        |       |        |
     |        |       |        +------------------------- 4. Company & Product ID Nos.
     |        |       |
     |        |       +---------------------------------- 3. Licence Count
     |        |
     |        +------------------------------------------ 2. CrypKey Version No.
     +--------------------------------------------------- 1. Unknown

The meaning of the "Unknown" field is obscure. Conceivably, it may consist of two separate eight-bit fields. To be sure, a given product installed several times on the same computer consistently places the same value in this field, regardless of the installation path. In addition, the leading byte, 0x02h, seems to be the same for the same product across different PCs, but the second byte can differ.

The "CrypKey Version No." field is essentially the same as that stored in the Site Code and reflects the CrypKey version that handled the product's licensing. It is merely the CrypKey version, expressed in decimal notation, multiplied by 10. The sample value of 0x36h = 54 means that CrypKey version 5.4 was used.

The "Licence Count" field is a signed eight-bit integer that reports the number of licences the original Site Key was good for. The field is essentially the same as that used in the Site Key.

The "Company & Product ID Nos." field is essentially the same as that in the Site Code. The lower 10 bits contain the company ID and the upper six bits the product ID. The sample value 0x2000h translates to a company ID of 0 and a product ID of 8, i.e. Kenonic's CKI.

The "Licence Kill Date" is a 32-bit date/time value similar to that in the ".rst" data block, which reports when the licence was removed. The value in the example, 0xBFA3B03Ah, corresponds to 16:22:18 on 19/10/2001, although the error of one day mentioned earlier makes the actual kill date equal to 18/10/2001.

As ever, the "Confirmation Code CRC" field ensures that the confirmation code has a CRC of zero so that CrypKey doesn't throw a wobbly.

8. Conclusions

The information in the preceding paragraphs is correct to the best of the author's knowledge. The two major facets of CrypKey's security, i.e. its usage of the hard disk drive and its cryptographic features, were discussed in some depth. Several implementation deficiencies and security holes were identified along the way, the two most severe of which are (a) the fact that cracking any one CrypKey protected program almost completely opens the door to every other one, and (b) the poor bit strength of the RSA subsystem. A 17-bit number can be factored within minutes on a piece of paper with the aid of a calculator.

The reader may have gained the impression that I take perverse delight in ripping to shreds the work of others. This is only true in cases where such work promises the earth and more, but, on closer examination, it turns out to consist of 95 per cent hype and five per cent substance. My delight is then in direct proportion to the extent to which I feel the originators of such feeble work are trying to dupe their customers into parting with their cash. In the present case, I am entirely at a loss to find some justification other than greed for the $1000.00-plus price tag attached to a full-featured CrypKey installation.

The primary purpose of the discussions is to warn potential CrypKey customers that this software protection and licensing solution is ultimately among the weakest on the market. With the information a customer gives Kenonic, they are in principle able to use the customer's product without the customer's knowledge. This applies whether the customer opts for the CrypKey SDK or the instant (CKI) suite, and, any promises by Kenonic aside, is a big red flag to my mind. It's a bit like buying a new car that the dealer also keeps a spare key to.

At the other end of the scale is the product user whose system can potentially be compromised by the shoddy implementation that largely characterises CrypKey. The software supplier in all probability does not even know that CrypKey might corrupt some customers' files because Kenonic have kept mum about it, either wittingly or out of pure ignorance. It matters little that such an event is very unlikely. It does matter that it could arise and has in fact done so, which does not augur well for the soundness of the remainder of CrypKey.

Lastly, there is the issue of the scattered remnants CrypKey leaves behind when a protected product is uninstalled. Understandable as this strategy may be, it does not accord with the idea that cleaning up behind oneself is an admirable character trait. All in all, the minutiae of CrypKey lead to a single possible conclusion: it's a lemon dressed like a cherry.

9. Appendix

A1. Pseudo-code implementation of CRYPT_RSA :

  CRYPT_RSA(BaseNum, KeyExp, KeyMod)
    If ((BaseNum = 0) and (KeyExp = 0)) or (KeyMod = 0) then
    Else If (BaseNum = 0) then
      TempProd <- 1
      TempBase <- BaseNum MOD KeyMod
      TempExpt <- KeyExp
      While (TempExpt != 0) do
        If NumberIsOdd(TempExpt) then
          TempProd <- (TempProd*TempBase) MOD KeyMod
        If (TempExpt > 1) then
          TempBase <- (TempBase*TempBase) MOD KeyMod
        TempExpt <- Truncate(TempExpt/2)

Note that CRYPT_RSA returns the value of (BaseNum^KeyExp) MOD KeyMod, where the "^" symbol denotes "raise to the power of". Furthermore, the function arguments "BaseNum", "KeyExp" and "KeyMod", as well as the result are unsigned integers.

A2. Pseudo-code implementation of CRYPT_STREAM:

  CRYPT_STREAM(DataIn, Direction, StreamKey)
    ResultBlock <- DataIn
    TempKey <- StreamKey
    For DataByte = 1 to ByteCount(DataIn) do
      TempByte <- ResultBlock[DataByte]
      For KeyByte = 1 to ByteCount(TempKey) do
        TempByte <- TempByte XOR TempKey[KeyByte]
      For KeyByte = ByteCount(TempKey) down to 2 do
        TempKey[KeyByte] <- TempKey[KeyByte] XOR TempKey[KeyByte-1]
      If (Direction = FWD) then
        TempKey[1] <- TempKey[1] XOR ResultBlock[DataByte]
      Else If (Direction = REV) then
        TempKey[1] <- TempKey[1] XOR TempByte
      ResultBlock[DataByte] <- TempByte

Note that "DataIn", "StreamKey" and the function result are blocks of bytes, and that the result is the same length as "DataIn".

A3. 32-bit date/time conversion :

This addendum describes a method of converting CrypKey's 32-bit date/time value to a more meaningful dd/mm/yyyy hh:mm:ss format. The conversion is done by means of an example value. Note that the answers may actually differ by a day or two from those obtained using other methods, e.g. the standard spreadsheet date format, but the correctness of the following method has been verified across the entire range of days that can be represented in this way, assuming that day zero is 01/01/1900.

  Input value = 0xBD43D405h = 3175339013
  1. Divide by 86400 (i.e. the number of seconds in a day)
     3175339013/86400 = 36751.608946759259259259...
  2. The integer part of the last result is the number of days since 01/01/1900
     NumYears = Truncate(36751/365) = 100
     NumLeaps = Truncate((NumYears-1)/4) = 24 (Note: 1900 was NOT a leap year!)
     .: Year = 1900+NumYears = 1900+100 = 2000
  3. Full days remaining after 01/01/2000
     36751-(365*NumYears+NumLeaps) = 36751-(36500+24) = 227
     (if this result is less than zero, then NumYears = NumYears-1, recalculate
      NumLeaps, Year & the full days remaining with the new NumYears & NumLeaps)
  4. Find the month (2000 was a leap year) from the remaining days
     J  F^ M  A  M  J  J  A  S  O  N  D
     31+29+31+30+31+30+31                = 213 <= 227
     31+29+31+30+31+30+31+31             = 244 >  227
     .: Month = August = 08
  5. Find the day
     .: Day = 1+227-213 = 15
  6. The fractional portion of the number calculated in 1. is the time of day
     24*0.608946759259259259... = 14.6147222...
     .: Hour = Truncate(14.6147222...) = 14
     60*0.6147222... = 36.88333...
     .: Minute = Truncate(36.88333...) = 36
     .: Second = Truncate(60*0.88333...) = 53
  7. Date/time = 15/08/2000 14:36:53

Converting a given date/time to a 32-bit representation, i.e. reversing the above conversion, is somewhat simpler :

  Input value = 27/03/1998 21:37:46
  1. NumYears = 1997-1900 = 98
     NumLeaps = Truncate((98-1)/4) = 24
     .: NumDays = 365*NumYears+NumLeaps = 365*98+24 = 35794
  2. Add the days since 01/01/1998
     J  F  M  ...
     31+28        = 59
     .:NumDays = 35794+59+(27-1) = 35879
  3. Calculate the result
     Result = 35879*86400+21*3600+37*60+46 = 3100023466 = 0xB8C69AAAh

Return to Main Index Return to Papers

© 1998-2003 exefoliator, published by CrackZ. 4th January 2003.