Você está na página 1de 29

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.

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)


Begin
fp_TempProd <- 1.0
If (Truncate(fp_KeyExp) <= 3) then
For Remainder = 1 to Truncate(fp_KeyExp) do
fp_TempProd <- fp_TempProd*fp_BaseNum
Else
Begin
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
End
Return(Truncate(fp_TempProd-fp_KeyMod*Truncate(fp_TempProd/fp_KeyMod)))
End

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)


Begin
If ((RSAExp = 0) or (RSAMod <= 1) or (RSAMod >= 0x20000h)) then
Return(ERROR)
WorkBlock <- DataIn
ByteInLen <- ByteCount(DataIn)
If (ByteInLen = 0) then
Return(0)
If (CryptType = ENCRYPT) then
Begin
WordInLen <- Truncate((ByteInLen+1)/2)
CarryLen <- Truncate((WordInLen+7)/8)
EmptyBlock(Bit17Block)
End
Else If (CryptType = DECRYPT) then
Begin
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)
End
Else
Return(ERROR)
DiffLen <- 2*WordInLen+CarryLen-ByteInLen
While (DiffLen > 0) do
Begin
WorkBlock <- ConcatBlocks(WorkBlock, 0x00h)
DiffLen <- DiffLen-1
End
BlockIndex <- 1
EmptyBlock(DataOut)
For ChunkNo = 1 to WordInLen do
Begin
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
Begin
Bit17Block <- BitShiftLeft(Bit17Block, 1)
If (ChunkData >= 0x10000h) then
SetLastBit(Bit17Block)
Else
ClearLastBit(Bit17Block)
End
Else
Bit17Block <- BitShiftRight(Bit17Block, 1)
ChunkData <- Last16Bits(ChunkData)
DataOut <- ConcatBlocks(DataOut, ChunkData)
End
If (CryptType = CK_ENCRYPT) then
Begin
DataOut <- ConcatBlocks(Bit17Block, DataOut)
DataOut <- CRYPT_STREAM(DataOut, FWD, E467F6A3)
End
Return(DataOut)
End

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 :

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

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 :

CRC16_CRYPKEY(903600206278FDC7)
= 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
CRC16_CRYPKEY(90058FA5785634120C001D41)
= 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 :

CRC16_CRYPKEY(00000020434B492E455845000000000000003837)
= 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

and

CRC16_CRYPKEY(0200F56D8721)
= 0x0000h

.....so 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

and

CRC16_CRYPKEY(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,

CRC16_CRYPKEY(02A536FB00203AB0A3BF6AD3)
= 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)


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

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)


Begin
ResultBlock <- DataIn
TempKey <- StreamKey
For DataByte = 1 to ByteCount(DataIn) do
Begin
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
End
Return(ResultBlock)
End

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
======================================[END]=======================================

Você também pode gostar