Chapter 19. Protecting Commercial Applications from Hacking

 

“Murphy was an optimist.”

 
 --Beck's Postulate

Setting Goals for Application Protection

Throughout the chapters of this book, we have looked at a variety of techniques for reverse engineering, hacking, eavesdropping, and cracking. In many of the chapters I was calling on the reader's conscience and good ethics to not abuse the intellectual rights of the software authors. Even though most users have fair moral values, it is the other few who can cause a lot of damage. This chapter offers practical advice on how to protect Java applications from hacking and implement a distribution model for commercial software products.

A typical Java application is delivered as a bundle (most of the time as a JAR or Zip file but sometimes as an executable installer) that contains Java and native libraries, configuration files, documentation, and various resource files. Today it is a common practice to offer a no-frills version of the software for free public use, with the full set of features available only in licensed versions. Another strategy to attract potential buyers is to allow a limited-time evaluation period during which the entire functionality is available. After the evaluation period, the commercial features of the application are disabled until a license is purchased. Borland uses such a strategy to distribute JBuilder X. It initially runs as a 30-day Enterprise Edition trial and then becomes the limited-functionality JBuilder X Foundation. Many enterprise software vendors offer their products free for development or rely on users' honesty and the fear of prosecution to encourage the purchase of the correct license. Regardless of the choice of the licensing and distribution model, each vendor is vitally interested in collecting the license fees to generate revenues. The simple techniques demonstrated in this chapter provide good insurance that the licensing model is followed.

It is important to understand that there is virtually no way to achieve absolute protection from hackers, especially when the application can be downloaded from the Internet. Even if the strongest security algorithm is used to produce and encrypt the sensitive data, such as a serial number, a good hacker with access to the application can patch the verification code to altogether bypass the checking. Previous chapters of this book have shown how easy it is to find and hack Java classes, and even the native code can be cracked if the stakes are high. Therefore, a key to a successful protection mechanism is to make it too difficult to crack for 95% of the typical users and to force the remaining 5% of experienced hackers into spending a significant amount of time on cracking. In other words, the goal is to make it cheaper to buy a license than to spend the time hacking the protection. Another vital aspect is to prevent the easy redistribution of hacked versions on the Net and to preclude hackers from being able to issue their own licenses for the product.

We will start by looking at key aspects of security and cryptography. Using Java Cryptography APIs, you will learn by example how to encrypt and decrypt information with ciphers, protect data integrity with a message digest, and implement a robust licensing mechanism with asymmetric key pairs. In addition to these measures, we will examine several techniques that protect the application core files from hacking and patching.

Securing Data with Java Cryptography Architecture

The word cryptography is based on the ancient Greek words kryptos (meaning hidden) and graphein (meaning writing). Cryptography provides a means of converting readable information into incomprehensible code that can be transmitted openly and then transformed back to its original form. Encryption is the process of encoding readable information into the code, and decryption is the process of extracting the readable information from the code. Another commonly used service of cryptography is producing a hash, or a message digest, to verify that a message has not been modified since it left the sender. Various mathematical algorithms are used to implement the cryptographic services. The algorithms can be grouped into three main categories by the type of service they provide: message digest, encryption/decryption, and signing.

Message digest algorithms do not modify the content of the message; rather, they produce a unique hash based on the message content and a secret key. The key can be anything—a number that is passed as a parameter to a computational algorithm, a string of characters used as a password, or a sequence of bytes. The sender of a sensitive message computes the digest using a secret key and sends it along with the message. The receiver uses the same secret key to compute the digest of the received message and, if it does not match the sender's digest message, the content is considered compromised. A third party who intercepts the message can view its content and modify it, but in the absence of the secret key, cannot recalculate the digest. Thus, the integrity of the communication is preserved.

Encryption/decryption algorithms serve the purpose of protecting sensitive information that can be intercepted by a third party. The message content is modified using a secret key, producing output that is virtually impossible to convert back to the original content. A third party who intercepts the message cannot decipher its content without the key.

There are two categories of encryption/decryption algorithms: the ones using symmetric keys and the ones using asymmetric keys. Symmetric algorithms require the sender and receiver to have the same exact key to perform encryption and decryption. Symmetric algorithms are sometimes referred to as two-way algorithms because the same key is used for encryption and decryption. The strength of protection obviously depends on how well the keys are protected from third-party access. Asymmetric algorithms use key pairs for transformations. A key pair consists of a public key and a private key. This type of algorithm is referred to as one-way because the information encrypted with a public key can only be decrypted with a private key and the information encrypted with the private key requires the public key. Generally, the public key is freely available to the world, whereas the private key is kept in secrecy by the owner. For client/server and server-side applications, this provides better security than symmetric algorithms because only the public key needs to be included in the client application distribution.

A typical example of asymmetric algorithm usage is a browser that needs to establish a secure communication with a Web server. The browser is given the public key to encode the information sent to the Web server. The Web server decrypts the information using its secretly held private key, but if a third party intercepts the message, it cannot decrypt it with the public key. The server uses the private key to encrypt the information sent to the browser so the entire communication is secure. Symmetric algorithms are much faster than asymmetric ones, which is why the two are often used in conjunction. For instance, SSL implementation establishes a session using an asymmetric algorithm. When the secure channel is created, symmetric keys are generated and exchanged for encryption of the transmitted data.

Signing refers to generating a relatively short digital signature based on arbitrarily sized data using a private key of an asymmetric algorithm. The signature is produced by the sender and is transmitted with the message. The receiver uses the public key and the signature to verify the integrity of the message. Just like a message digest, the signature is mathematically unique for the given data, so if the data has been modified, the signature does not match the content. Authenticity is ensured through the use of an asymmetric algorithm, in which only the sender has the private key. To prevent a third party from forging a public key and claiming it to be the sender's key, digital certificates are commonly used. A digital certificate contains the public key of the sender that is signed by a public key of a trusted certificate authority (CA). For instance, browsers are preconfigured to trust Verisign (http://www.verisign.com) as a CA. A company that wants to allow users to establish a secure communication channel with its Web server must send its public key to Verisign to obtain a digital certificate. When the certificate is obtained, it is installed on the Web server to be handed to the browser at the communication initiation. The browser verifies the authenticity of the certificate using Verisign's public key and establishes a secure connection only if the verification succeeds.

Java Cryptography Architecture Overview

Java Cryptography Architecture (JCA) provides a complete and robust implementation of cryptography services and algorithms. Like most of the Java APIs, JCA provides interfaces that define how an application can interact with the services in a vendor-neutral way. Java Cryptography Extensions (JCE), which was once a separate module, is now a part of J2SE starting from JDK 1.4. JCE comes with a Sun provider that implements the most commonly used algorithms, such as HmacSHA1 for secure hashing and DES for key pair generation and signing. Because of U.S. government export restrictions, some algorithms such as RSA are not included in the JDK. In addition to Sun JCE, other excellent open-source packages implement a rich set of algorithms. Bouncy Castle (http://www.bouncycastle.org) and Cryptix (http://www.cryptix.org) provide Java implementations and can be downloaded and used free.

The core JCA classes are in the javax.crypto package, although the classes and interfaces for working with message digests are found in the java.security package. The Java Security home page (located at http://java.sun.com/j2se/1.4.2/docs/guide/security) is a good starting point for getting the Java-centric details on various security topics. The JCE home page at http://java.sun.com/products/jce/index-14.html provides a high-level overview of JCE and links to JCE-related pages such as the reference guide. If you like to dig deep, I recommend buying a book on Java security because the subject is vast and interesting. Java Security Handbook by Jamie Jaworsky and Paul Perrone (Sams Publishing, ISBN: 0672316021) offers comprehensive coverage of various security topics. The focus of the following sections of this chapter is on the practical use of security to protect Java applications from hacking.

Securing Chat Messages with JCA

Once again, I will use the notorious Chat application to illustrate the most useful methods of safeguarding user privacy and the author's intellectual property. Because Chat sends messages across the network, the user conversation is prone to interception and eavesdropping by a third party. The most obvious starting point to secure Chat is therefore the protection of transmitted message content.

Recall that Chat uses RMI over TCP/IP to exchange messages between instances running on different hosts. This is not as bad as HTML over HTTP because the binary TCP/IP streams are much harder to eavesdrop on than the text-based HTTP. Still, as you saw in Chapter 13, “Eavesdropping Techniques,” with the right tools a hacker can listen to the conversation and read the message content. The main reason eavesdropping is possible is that the strings inside the serialized Java objects remain as text. Securing the Chat messages therefore requires encrypting the strings. Just about any kind of encryption will work for Chat because the messages are binary and, as long as the strings are not human recognizable, they do not stand out in the body of the message (see Chapter 13). Even simple XORing of characters works. In theory, the most secure way to protect the RMI communication channel is to use custom socket factories that create SSL sockets. However, because we are interested in learning a generic method for data protection, we will code with the Java Cryptography API.

The first design decision is which algorithm to use. Asymmetrical algorithms generally offer better protection because the private key is not available to the general public. However, in the case of Chat, an asymmetric algorithm is not the right solution for message encryption. The Chat application installed on a desktop should be capable of both encrypting the messages it sends and decrypting the messages it receives. Using an asymmetric algorithm would mean shipping both private and public keys with the Chat distribution. This effectively negates the extra protection you would get from the asymmetric algorithm, so you should use symmetric encryption because it performs better and is easier to write.

The second design decision is which security provider to use. The provider gives a concrete implementation of a particular algorithm. To avoid having to redistribute additional libraries with Chat, let's first check on the algorithms implemented by Sun JCE because it is bundled with the JRE. Sun JCE supports the following cipher algorithms: Data Encryption Standard (DES), DESede, and PBEWithMD5AndDES. DES is a widely used standard that has been adopted by the U.S. government. Even though there are known ways to crack it with a lot of computing power, it provides adequate protection for most applications. DESede, also known as multiple DES, uses multiple DES keys for extra strength. PBEWithMD5AndDES uses a combination of algorithms that includes a password-based encryption defined in the PKCS#5 standard and a message digest from the MD5 and DES algorithms. Because of the standardization specified by JCA, the client code that draws on these algorithms is virtually independent of the algorithm used. We'll select PBEWithMD5AndDES because it offers the strongest protection of the three.

A picture is worth a thousand words, and in the world of programming, the source code is worth a thousand pictures. All the source code we will be working with in this chapter is located in the covertjava.protect package. We will begin by looking at a class that provides the encryption services for the Chat application. Listing 19.1 shows the constructor of covertjava.protect.Encryptor.

Example 19.1. Preparing Ciphers for Encryption and Decryption

import javax.crypto.*;
import javax.crypto.spec.*;

public Encryptor(char[] password) throws Exception {
    PBEKeySpec keySpec = new PBEKeySpec(password);
    SecretKeyFactory keyFactory =
            SecretKeyFactory.getInstance("PBEWithMD5AndDES");
    secretKey = keyFactory.generateSecret(keySpec);

    PBEParameterSpec paramSpec = new PBEParameterSpec(this.keyParams, this.iter_count);
    this.encCipher = Cipher.getInstance("PBEWithMD5AndDES");
    this.encCipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec);
    this.decCipher = Cipher.getInstance("PBEWithMD5AndDES");
    this.decCipher.init(Cipher.DECRYPT_MODE, secretKey, paramSpec);
}

Let's dissect the source code and understand what is being done. The Encryptor constructor takes in a password as a character array. The PBEWithMD5AndDES algorithm uses three parameters: It passes the salt and the iteration count to initialize the DES algorithm and passes the password used for encryption with PKCS#5. The JCE class that represents an encrypting algorithm is javax.crypto.Cipher. A program obtains an instance of a cipher by calling the Cipher.getInstance() method, which takes the algorithm name (there is an overloaded method that can also take the provider name).

Security algorithms often require parameters to be supplied by the client code. The parameters are used in mathematical calculations performed during the encryption, and they represent a secret seed or a password that is required to decrypt the data later. Even though most of the algorithms that require parameters can use the default values supplied by the provider, it is highly recommended to initialize them with custom values.

There are two ways to initialize our cipher. One is to provide algorithm parameters such as the salt and the iteration count. Another is to provide an already generated key. If we were to choose to provide a key, we would have to ship the key with the distribution of Chat, which makes it easier for hackers to extract the key. The algorithm parameters, which are regular numbers, can be hardcoded into the Java code and placed in different classes. Obfuscation makes the code very difficult to read, so we will opt for providing the parameters instead of the key. Listing 19.2 shows the declaration of the algorithm parameters inside the Encryptor class.

Example 19.2. PBEWithMD5AndDES Parameters Used by Encryptor

public class Encryptor {
    private static byte[] keyParams = {
        (byte)0x10, (byte)0x15, (byte)0x01, (byte)0x04,
        (byte)0x55, (byte)0x06, (byte)0x72, (byte)0x01
    };
    private static int iter_count = 20;

    ...
}

In a real-life application, it would be better to place the parameters in a different class or generate them on-the-fly using a random number generator with the hardcoded seed. That would make hacking the application harder, but we'll keep things simple. Looking back at Listing 19.1, we can see that the first block of code creates a key specification based on the provided password. Key specification is an intermediate form of key data, which is used by the factory to generate a secret key. Because all instances of Chat use the same algorithm parameters and the password, the generated keys are identical. This means that messages encrypted by one Chat instance can be decrypted by another instance. After the secret key is generated, two instances of the cipher are obtained. One of them is initialized for encryption, and the other is initialized for decryption.

After an instance of Encryptor is constructed, it is ready to perform encryption and decryption. Most cryptography algorithms deal with raw bytes. The cipher class has two methods—update() and doFinal()—that can be used to encrypt an array of bytes. For instance, if we have an initialized cipher named cipher and an array of bytes named data, the data can be encrypted as follows:

byte[] ecryptedData = cipher.doFinal(data);

JCE has a utility class called SealedObject that wraps around any serializable object and uses a provided cipher to encrypt or decrypt the wrapped object during the serialization. Because Chat sends messages as objects, SealedObject is a better choice than raw byte data because it provides a higher-level API to encryption. The two methods provided by Encryptor for encrypting and decrypting instances of java.io.Serializable are shown in Listing 19.3.

Example 19.3. Methods That Implement Encryption and Decryption

public Serializable encryptObject(Serializable object) throws Exception {
    return new SealedObject(object, this.encCipher);
}

public Object decryptObject(Serializable object) throws Exception {
    return ((SealedObject)object).getObject(this.decCipher);
}

As you can see, SealedObject makes the implementation trivial. The Encryptor class we have discussed can now be used in the Chat application. Rather than passing instances of covertjava.chat.MessageInfo to each other, Chat would call Encryptor to obtain the encrypted version of the message before sending it. When a new message is received, Chat would use Encryptor to extract the MessageInfo object from the received sealed object. This code would have to be placed in ChatServer's sendMessage() and receiveMessage() methods, but we are not going to do this because we want to save time and space. The main() method in the Encryptor class shows a self test that writes and reads a text string to a file.

Protecting Application Distribution from Hacking

Encrypting the transmitted data protects the information from eavesdropping at the protocol level. This safeguards the user's information, but not the intellectual property in the software such as the algorithms, design patterns, and code. Many reverse-engineering techniques presented in this book can be easily used to crack a commercial product and unlock the functionality that would otherwise require a purchase of a license. For licenses that are issued based on the number of hosts where the software is installed, another potential threat can come from an unethical organization buying the cheapest license for one host and then rolling it out to a large number of hosts. This section discusses several techniques that protect the application distribution from hacking and ensure that the fees are paid according to the licensing model.

Protecting Bytecode from Decompiling

Chapter 2, “Decompiling Classes,” has shown how easily you can obtain the source code from Java bytecode and that, in most cases, the decompiled code is virtually a one-to-one match to the original source code. Chapter 3, “Obfuscating Classes,” provided details on how bytecode can be protected from decompiling. It should be obvious that the strength of the overall protection is as strong as the code that implements it. You can use the strongest algorithm to encrypt the data, but if the code can be decompiled and patched in 30 minutes, the encryption can be simply commented out.

Obfuscation, obfuscation, obfuscation. That is the only reliable way to protect the bytecode and therefore the intellectual property of an application. Control flow obfuscation, which was covered in Chapter 3, is crucial to achieve the best results. The ultimate countermeasure against decompiling bytecode is to compile the Java application into a native executable. We have looked at the complexity of reverse engineering and patching the native code and, no matter how good the bytecode obfuscator is, the native code is much harder to crack. Unfortunately, by now most vendors that were offering Java to native code compilers have either gone out of business or stopped actively supporting their products. The JIT improvements and the increasing processor speeds provide enough performance for Java applications, eliminating the need to compile into the native code. TowerJ and Excelsior probably have the best implementations for Windows, but I advise caution and thorough testing of the compiled application to ensure that all the features are properly functioning. For most Java applications, using an aggressive obfuscator such as Zelix KlassMaster is probably a better choice than compiling the code into the native binaries.

Protecting Bytecode from Hacking

No matter how good of a job is done by the obfuscator, the bytecode can still be decompiled. And if it can be decompiled, it can be modified and the application can be patched. To strengthen the protection, we will review a few ideas on safety checks that can safeguard classes from patching.

Throughout the book, we've developed various techniques for hacking and patching. We have discussed how to decompile and then patch entire classes, access protected and private methods, and work with the system boot class path. Used for the wrong reasons, those techniques can harm the intellectual property inside a Java application. Here we look at the techniques that make hacking much harder and the countermeasures for each.

Hacking Non-Public Methods and Variables of a Class (Chapter 4)

The easiest solution to this is to seal the application JAR. Sealing a JAR guarantees that all the classes in a package come from the same code source. This means that a hacker cannot place custom classes in the packages supplied by the JAR. JAR sealing is achieved by adding the following line to the manifest file:

Sealed: true

The JAR itself needs to be protected from modifications. Just as you can easily seal it, a hacker can easily unseal it. All the hacker would have to do is unjar the contents of the JAR file to a temporary directory, remove the Sealed attribute, and then rejar it back. Java supports the notion of signed JARs that can protect its contents from modifications by signing every class in it with a digital signature. This works well for signed applets that are downloaded and verified by the browser. The problem is that the signed JAR itself is not protected, so once again a hacker can unjar the file, remove the manifest with the digital signatures, and rejar the file. Even though the JAR would no longer be considered authentic and originating from its true vendor, it could be executed and used just fine. Thus, you need a way to ensure that the application distribution contents are not modified; we look at this in the following section.

Replacing and Patching Application Classes (Chapter 5)

Sealing a JAR provides a remedy for this hacking technique as well. For extra protection, you can add a check asserting that a class is indeed loaded from the application distribution JAR and not from a third-party JAR. The implementation of this simple method is provided in covertjava.protect.IntegrityProtector. Listing 19.4 shows the source code for assertClassSource().

Example 19.4. Asserting Class Source JAR

public void assertClassSource(Class cls, String jarName) {
    // Class loader should not be null
    if (cls.getClassLoader() == null)
        throw new InternalError(BOOT_CLASSLOADER);

    String name = cls.getName();
    int lastDot = name.lastIndexOf('.'),
    if (lastDot != -1)
        name = name.substring(lastDot + 1);
    URL url = cls.getResource(name + ".class");
    if (url == null)
        throw new InternalError(FAILED_TO_GET_URL);

    name = url.toString();
    if (name.startsWith("jar:") == false || name.indexOf(jarName + "!") == -1)
        throw new InternalError(UNEXPECTED_JAR);
}

The first if statement ensures that the class loader of the given class is not the boot class loader by comparing it with a null. This assertion can be made because you know that none of the Chat classes are placed on the boot class path, which means they should be loaded using the default application launcher's class loader.

The rest of the method code obtains a URL for the source of the CLASS file that was used to create the given class. This is the URL returned by Class.getResource() for the MessageInfo class:

jar:file:/C:/Projects/CovertJava/distrib/lib/chat.jar!/covertjava/chat/MessageInfo.class

The URL indicates that the class was loaded from the chat.jar file located in the C:/Projects/CovertJava/distrib/lib directory. After obtaining the URL, assertClassSource() ensures that it starts with jar: and contains the name of the JAR file that was passed as a method parameter. An unchecked InternalError is thrown to abort the execution if the asserting fails. This might not be a completely foolproof verification, but it should be good enough to thwart most attempts at patching. To take advantage of this protection, the Chat application must invoke assertClassSource() on the key classes that are prime candidates for patching. We will add a new class, ProtectedChatApplication, as an alternative entry point for the Chat application. ProtectedChat will extend covertjava.chat.ChatApplication and use various protection mechanisms developed in this chapter. The code in Listing 19.5 shows a portion of ProtectedChatApplication's main() method that asserts the origin of the LicenseManager class.

Example 19.5. Asserting the Origin of LicenseManager

public static void main(String[] args) throws Exception {
    LicenseManager licenseManager = new LicenseManager("conf/chat.license");
    IntegrityProtector protector = new IntegrityProtector();
    protector.assertClassSource(licenseManager.getClass(), "/lib/chat.jar");
    ...
}

The main() method ensures that the LicenseManager class is loaded from the chat.jar file located in the lib subdirectory. We should insert checks like that in other classes of Chat. The more checks we use, the more work a hacker must put in to crack the application.

Manipulating Java Security (Chapter 7)

Manipulating Java security enables hackers to gain access to protected, package, and private members of a class and to bypass other security checks normally enforced by a security manager. If an application installs a security manager or uses a custom policy file, you should insert checks that assert that the security manager is installed and that the correct policy file is used. The security manager can be obtained using System.getSecurityManager(), and to verify the original policy file you can check the value of the java.security.policy system property. The policy file itself can be protected from modifications using the application content protection technique described later in this chapter.

Reverse Engineering Applications (Chapter 12)

Depending on the type of the resource, the protection requires either bytecode protection or application content protection. Resources such as menu item strings and error messages are often hardcoded in the bytecode, whereas resources such as images and media files are typically stored in a separate directory or inside a JAR file. The bytecode protection was reviewed earlier, and the application content protection is presented in the next section.

Controlling Class Loading (Chapter 14)

Custom class loaders provide a lot of power because they can manipulate the bytecode on-the-fly. It is certainly not a common technique to hack applications, but if you want to protect an application class from runtime bytecode manipulations, you can install and use a predefined custom class loader instead of the system class loader. Then, in various places of the application code, a check can be made to see whether a class was loaded with the expected class loader.

Understanding and Tweaking Bytecode (Chapter 17)

Bytecode tweaking requires either a custom class loader that performs the tweaking on-the-fly or static modifications to the application CLASS files. We have discussed how to prevent the use of a third-party class loader in the previous paragraph, and the next section describes how to protect the application distribution files.

Protecting Application Content from Hacking

Application content here refers to the files distributed with the application. This includes the libraries as JAR and Zip files, images, configuration files, and other content. Ensuring that the key application files are not modified is critical for application integrity protection because most hacking techniques require changing some file. The most important type of file that needs integrity protection is the JAR archive. We have already looked at a trick that can ensure that the class is loaded from the expected JAR. Now we will develop a class allowing an application to assert that none of its files have been tampered with.

The most straightforward way of verifying the integrity of the application content is to iterate the distribution files and check the attributes, such as size and modification time. For ultimate protection, the application can produce a checksum of the file content and verify it against the checksum of the original files taken at the distribution preparation time. We are going to develop a class called IntegrityProtector in the covertjava.protect package and use it to protect the infamous Chat application. To keep the example concise, we will limit the verification to file lengths, although it can easily be extended to include other file attributes and the content hash. IntegrityProtector iterates a list of key application files, produces a total size of files in bytes, and then calculates a checksum using the message digest algorithm. We will then add a configuration file to Chat that stores the version and the digest of the application distribution. Storing the digest instead of the total length of files makes hacking much harder. Finally, at Chat application startup we will use IntegrityProtector to assert that the current checksum of the distribution files matches the original checksum provided in the configuration file.

Our first task is to decide which files in Chat should be protected from modifications. We cannot simply include all the files because certain files are meant to be changed by the end user. For instance, bin/setenv.bat can be modified to provide a specific home directory for Chat or to run it on a different port. conf/log4j.properties can change if a user adjusts the logging levels. However, files such as lib/chat.jar and conf/java.policy should never differ from the original versions (unless we want to send the customer patches, in which case the new checksums can be provided with the patch). In this example, we will protect only the core files of the Chat distribution:

confjava.policy
libchat.jar
liblog4j-1.2.8.jar

For flexibility of design, we will read the list of core files from a configuration file called ChatFileList.class. To confuse hackers, we gave the list file a .class extension, although its content is text. During the development, this file will be kept in the CovertJava/conf directory, but we will modify the build.xml file to copy ChatFileList.class into the covertjava/protect directory together with the classes from the covertjava.protect package. Listing 19.6 shows the <copy> task that has been added to build.xml.

Example 19.6. Copying the File List into the Distribution Directory

<copy todir="classes/covertjava/protect">
    <fileset dir="conf">
        <include name="ChatFileList.class"/>
    </fileset>
</copy>

Spending an extra 15 minutes to disguise the file list as a regular class file is worth the effort because it makes a trivial protection method less obvious to a hacker. We can now proceed with the development of the IntegrityProtector class in the covertjava.protect package. We first must write a few helper methods that read the contents of a text file (such as ChatFileList.class for Chat) and parse it to produce an array of strings. If you open the IntegrityProtector.java file in the src/covertjava/protect directory, you can see the implementations of the helper methods: readFilePathsFromResource(), readFilePathsFromFile(), and readFilePathsFromString(). Now we can code a method that produces a checksum for a given list of file paths. We then add the getFilesCheckSum() method to IntegrityProtector and implement it as shown in Listing 19.7.

Example 19.7. Calculating a Checksum for a List of Files

public String getFilesCheckSum(String[] paths, char separator,
        String installPath) throws Exception {
    long totalSize = 0;
    for (int i = 0; i < paths.length; i++) {
        String path = paths[i];
        if (separator != File.separatorChar)
            path = path.replace(separator, File.separatorChar);
        path = installPath + File.separatorChar + path;
        totalSize += new File(path).length();
    }

    byte[] checkSum = toByteArray(totalSize);
    MessageDigest sha = MessageDigest.getInstance("SHA-1");
    checkSum = sha.digest(checkSum);
    BASE64Encoder encoder = new BASE64Encoder();
    return encoder.encode(checkSum);
}

The method iterates the array of filenames, adding the size of each file in bytes to the total size. After the total size is calculated, it is converted to an array of bytes using a helper method called toByteArray(). getFilesCheckSum() then obtains an instance of a message digest algorithm SHA-1 (Secure Hash Algorithm provided by Sun JCE) and gets a hash of the total size. Because the checksum has to be stored in a text file, we need to convert the bytes into human-recognizable ASCII characters. We cannot simply cast byte variables to char type because it produces nonprintable characters (for instance, the byte value of 7 would produce a beep, and the byte value of 8 would produce a backspace). The standard solution to this problem is base64 encoding. Base64 encoding uses a subset of ASCII code that contains only 64 printable characters. The subset includes characters A–Z and a–z, numerals 0–9, and a few other safe characters such as punctuation marks. Because fewer characters are used, base64 allocates 6 bits per character instead of the 8 bits used for ASCII characters. Consequently, 3 bytes of input data are encoded into 4 bytes of output data. IntegrityProtector uses the Base64Encoder class, found in the sun.misc package, to obtain a printable representation of a file's checksum.

Now that we are able to obtain the file's checksum, we will use it to verify the integrity of the Chat installation. We will code IntegrityProtector's main() method to output the checksum for a given file list. Listing 19.8 shows the body of the main() method.

Example 19.8. Outputting a File's Checksum

public static void main(String[] args) throws Exception {
    if (args.length != 1) {
        System.out.println("Syntax: IntegrityProtector " +
              "[-Dhome=<path to home>] <file list path>");
        System.exit(1);
    }

    IntegrityProtector protector = new IntegrityProtector();
    String[] paths = protector.readFilePathsFromFile(args[0]);

    String homePath = System.getProperty("home", "..");
    String checksum = protector.getFilesCheckSum(paths, '', homePath);
    System.out.println("Checksum = [" + checksum + "]");
}

The main() method takes in one parameter that specifies the list file (conf/ChatFileList.class in our case) and an optional parameter giving the home directory for files (distrib in our case). For convenience, we have included a batch file getChatChecksum.bat in the CovertJava/bin directory that uses IntegrityProtecor to output the Chat checksum. Running getChatChecksum.bat after building the Chat distributing with the Ant release task produces the following output:

Checksum = [gLmBOKQe88gLrC9vaSjBarf2Rfw=]

Every time a core file of Chat changes (for instance, when you rebuild lib/chat.jar) the checksum is different. But if the file sizes do not change, the checksum remains the same, which potentially opens a hole in protection. This is why getting a checksum based on the actual file content is more secure; however, most hackers will not bother to keep the file size unchanged, so even our simple mechanism would work for Chat.

We can now add a configuration file called chat.properties to the Chat conf directory. Inside the file, we will store the checksum of the Chat distribution as the value of the chat.versionInfo property. Once again, we avoid using an intuitive name for the property to make hacking harder. Our final task is to ensure that, at the start, the Chat application verifies the current checksum for its files against the checksum read from the configuration file. The portion of ProtectedChatApplication's main() method that does it is shown in Listing 19.9.

Example 19.9. Verifying the Current Checksum Against the Distribution Checksum

public static void main(String[] args) throws Exception {
    String homePath = System.getProperty("chat.home");
    String propPath = homePath + File.separator + "conf" +
                      File.separator + "chat.properties";
    AppProperties props = new AppProperties(propPath);
    String checkSum = props.getProperty("chat.versionInfo");
    IntegrityProtector protector = new IntegrityProtector();
    String[] paths = protector.readFilePathsFromResource("ChatFileList.class");
    protector.assertFilesIntegrity(paths, '', homePath, checkSum);
}

After reading the original checksum and the list of protected files, the method uses IntegrityProtector's assertFilesIntegrity() method to ensure the integrity of the content. assertFilesIntegrity(), shown in Listing 19.10, simply invokes getFilesChecksum() for the given list of files and throws an InternalError if the calculated checksum does not match the original checksum.

Example 19.10. IntegrityProtector.assertFilesIntegrity() Implementation

public void assertFilesIntegrity(String[] paths, char separator,
                  String installPath, String checkSum) throws Exception {
    String installCheckSum = getFilesCheckSum(paths, separator, installPath);
    if (installCheckSum.equals(checkSum) == false)
        throw new InternalError("Some of the installation files are corrupt");
}

With all the coding done, we can test our protection. The Chat files provided for this book are shipped with the correct checksum. You should be able to run Chat using chat_protected.bat from the distrib/bin directory. Verify that you can bring up the main Chat window on your machine. Now let's pretend we are hacking Chat by modifying java.policy in the distrib/conf directory. Open that file, add a new line, and save it. Be sure that the file length has changed, and try running chat_protected.bat again. You should see the following exception:

Exception in thread "main" java.lang.InternalError:
Some of the installation files are corrupt
    at covertjava.protect.IntegrityProtector.assertFilesIntegrity(...)
    at covertjava.protect.ProtectedChatApplication.main(...)

Because the file length has changed, the calculated checksum no longer matches the original checksum and IntegrityProtector throws an error. If we were to distribute the protected implementation of Chat, we would of course not ship the getChatChecksum.bat file and would remove distrib/bin/chat.bat along with the main() method of ChatApplication. This would ensure that the only way to launch Chat is through the ProtectedChatApplication class. To enable the protected Chat to run with the new version of files, we would have to obtain the new checksum and set it as the value of the chat.versionInfo property in the chat.properties file.

Implementing Licensing to Unlock Application Features

This section examines the way in which the applications are licensed today and then discusses how to develop a licensing framework for a commercially distributed application.

Modern Software Licensing Models

Several dominant license models govern the distribution of modern software. The terms of distribution and use are typically written in the end user license agreement (EULA) that is shipped with the product. Although each vendor has a choice of writing out the licensing terms, the license models can be grouped in the following three categories.

Closed Source Commercial Software

This is the traditional model for distributing for-profit software. It includes proprietary products such as Microsoft Windows; software that can be downloaded for a free evaluation, such as ItelliJ IDEA; and products that have a limited-functionality free edition, such as Borland JBuilder and BEA WebLogic. Offering a limited-functionality free edition is becoming more and more popular with Java vendors because developers like to get a good feel for a product before they make their purchase decision. When a product is well written, the users get accustomed to it and in the end often decide to buy the fully functional version.

Open Source Commercial Software

This emerging licensing model is gaining popularity and enables end users to not only download and use the software, but to also obtain the source code. The terms of use typically allow free development and deployment but might require fees for documentation, technical support, or advanced features. The most prominent examples in this category are the JBoss application server and MySQL database.

Open Source Free Software

Software in this category is made available to the public free without any restrictions. The most common license used for open source free software is the General Public License (GPL) that allows the use of the software and its source code for commercial and noncommercial use. There are variations of GPL and other open source licenses, such as Apache, that might impose certain restrictions on the software use, but they all strive for the basic idea of free-for-all.

Implementing Licensing to Unlock Commercial Features

When the source code is provided with a product, it is obvious that having programmatic restrictions to enforce the licensing model is pointless. Any user can easily remove restrictions on the functionality by modifying the source code and rebuilding the product. However, the majority of products today are not shipped with the source code, so programmatic enforcement of the licensing policy can help in generating sales. We are going to develop a LicenseManager class that produces secure serial numbers based on the customer environment and the license type. The class will use an asymmetric algorithm to ensure that only the software vendor can issue the serial numbers. Even if you do not need to implement license management, you will benefit from reading this section because it demonstrates a practical use of Java security and cryptography APIs.

Deciding on the licensing design requires consideration of the most effective way to prevent licensing policy abuse without sacrificing the customer's experience with the product. For instance, issuing a license that unlocks commercial product features without tying it to the end user's environment is unsafe. This kind of license might be easier to issue because it does not require the user sending the information to the product vendor, but it can turn into a distribution nightmare if somebody places the license on the Internet. You should attach the license to a parameter in the customer environment such as the hostname, IP address, or domain name. That way, even if the license surfaces on the Internet, it will not work in an environment for which it was not issued. Another important consideration is the ability to enable restricted features of the product through the license file without having to maintain and build multiple versions of the software. Expiration time, embedded into the license file, can be useful if the vendor wants to issue a temporary license for product evaluation.

For instance, if Chat had commercial potential, we could have distributed a free edition with limited functionality that would allow sending plain-text messages to one user at a time. Then we could have implemented extra features such as HTML text, colors, buddy lists, smiley faces, and images support. Theoretically, we could have built two instances of Chat—the limited one and the full-featured one. But, in practice, maintaining that type of code is very difficult so, like most vendors, we prefer to keep one code base of the full-featured version. To restrict access to a commercial feature, we must insert checks in certain key parts of the code to test whether a license file exists and whether it allows that feature. If the checks fail, the feature is disabled. We then could offer Chat without a license file for a free download. If a user wanted to enjoy the advanced features of our wonderful application, he would have to purchase a license. After payment was received, we would issue a license based on the user's hostname and send the license file to him. The next time the user runs Chat, the application would read the license information and enable the purchased features.

The information about which features to allow and which ones to disable can be encrypted in the license file, but that makes the file hard to read and maintain. Storing the license parameters in plain text is easier, but that would be like dangling a piece of chicken in front of a hungry crocodile. For instance, a license can be issued for a specific host, but even the least-sophisticated user can copy the license file to another host and change the value of the hostname. The cleanest solution is to produce a secure digital signature based on the license parameters and store it in the license file together with the parameters in plain text. The signature is then generated by a licensing utility using a private key of an asymmetric algorithm. At startup, the application would use a public key of the algorithm and the signature to verify the authenticity of the information to be read from the license file. Only if the verification succeeds would the license restrictions be removed.

Creating a License File

Let's proceed with the development of a generic license manager and use it with Chat. For simplicity, we will use the Java properties file format for the license file. The licensing will be based on three parameters: the hostname, IP address, and expiration date. Because I didn't want to spend time writing all those money-generating features of Chat, this simple example is good enough to illustrate the approach. Create a new file called chat.license in CovertJava/distrib/conf that looks as shown in Listing 19.11.

Example 19.11. Chat License File

host=localhost
ip=172.24.109.159
expires=2005/1/1
serial=

Use your hostname and IP address and leave the value of the serial property blank for now—we'll get to it later. We must code two classes, one for the license generation and another for verification. We do not want to ship the license generation code with the application distribution, so two classes are necessary. Because both classes have to read the license information from a file, it would make sense for one of them to extend the other. Let's start with the LicenseManager class in the covertjava.protect package. We'll define member fields for the license properties (see Listing 19.12).

Example 19.12. LicenseManager Declaration

public class LicenseManager {
    private String host;
    private String ip;
    private Date expires;
    ...
}

Then we'll give it a constructor that takes the license filename as a parameter and populates the internal fields with the license information, as shown in Listing 19.13.

Example 19.13. LicenseManager Initialization

public LicenseManager(String licenseFileName) throws Exception {
    this.licenseProps = new AppProperties(home+File.separator+licenseFileName);
    this.host = licenseProps.getProperty("host");
    this.ip = licenseProps.getProperty("ip");
    String expiresString = licenseProps.getProperty("expires");
    this.expires = this.dateFormat.parse(expiresString);
    ...
}

To protect the license parameters from modifications, we need to produce a digital signature. All JCE algorithms work with arrays of bytes, so we'll add a getLicenseString() method that returns a unified representation of all the license properties. The source code for this method is shown in Listing 19.14.

Example 19.14. Unified Representation of License Properties

protected String getLicenseString() throws Exception {
    return this.host + this.ip + this.expires;
}

For now, we can leave the LicenseManager class and start working on LicenseGenerator. LicenseGenerator should extend LicenseManager and provide methods to generate the digital signature. We will use base64-encoded digital signature as the serial number. To generate a signature, we also need a pair of keys for use with the asymmetric algorithm. The keys can be generated using JDK's keytool utility or programmatically using Java security APIs. With keytool, the keys can be generated and exported with just a few commands, but we'll take the programmatic approach for academic interest. First, we need to decide on the algorithm to use and the key length. The standard choices for asymmetric encryption are the DSA and RSA algorithms. Both offer adequate protection with the right key size, but we'll use DSA because it is natively supported by Sun JCE, which is shipped with the JRE. The key size directly affects the complexity of encryption: The longer the key, the harder it is to crack. Every bit doubles the cracking time. Whereas 16-bit keys can be cracked by a modern CPU in a matter of minutes, 1024-bit keys are deemed impossible to crack because, even using all the silicon power on earth, the time required to crack one would run into millions of years (or so they say). Because we are not doing real-time decryption, we'll use the 1024-bit key size. The code in Listing 19.15 shows a method of LicenseGenerator that generates a pair of keys.

Example 19.15. Generation of Keys for the DSA Algorithm

public void generateKeys() throws Exception {
    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DSA");
    SecureRandom random = SecureRandom.getInstance("SHA1PRNG", "SUN");
    random.setSeed(System.currentTimeMillis());
    keyGen.initialize(1024, random);
    KeyPair pair = keyGen.generateKeyPair();

    String publicKeyPath = home + File.separator + "conf"
                                + File.separator + "key_public.ser";
    byte[] bytes = pair.getPublic().getEncoded();
    FileOutputStream stream = new FileOutputStream(publicKeyPath);
    stream.write(bytes);
    stream.close();
    ...
}

The implementation first obtains an instance of the DSA key pair generator. The generator needs to be initialized with the key size (1024 bits) and a random numbers provider (we use Sun's SecureRandom). After initializing the generator, keys are generated via a call to generateKeyPair(). After the pair is generated, the remaining task is to save the public and private keys to disk. Listing 19.15 shows how the public key was saved to the key_public.ser file in the Conf directory. The remaining part of the method that is not shown in Listing 19.15 saves the private key to the key_private.ser file in the same way.

Obviously, we want to generate the keys only once. LicenseGenerator is given a main() method that calls generateKeys() or generateSerialNumber(), depending on the command-line parameters. We've already seen the implementation of key generation, so let's look at generating the serial number. As mentioned earlier, the serial number is generated as a base64-encoded digital signature for the unified license properties. The generateSerialNumber() method shown in Listing 19.16 does just that.

Example 19.16. Generating a Serial Number

public String generateSerialNumber() throws Exception {
    String licenseString = getLicenseString();
    byte[] serialBytes = licenseString.getBytes(CHARSET);
    serialBytes = getSignature(serialBytes);
    BASE64Encoder encoder = new BASE64Encoder();
    return encoder.encode(serialBytes);
}

The bytes of the serial number string are passed to the getSignature() method, the output of which is then converted to a string using the BASE64Encoder class. This brings us to the implementation of digital signing with the DSA algorithm, which is shown in Listing 19.17.

Example 19.17. Digital Signing with DSA

private byte[] getSignature(byte[] serialBytes) throws Exception {
    String privateKeyPath = home + File.separator + "conf" +
                                   File.separator + "key_private.ser";
    FileInputStream stream = new FileInputStream(privateKeyPath);
    byte[] encodedPrivateKey = new byte[stream.available()];
    stream.read(encodedPrivateKey);
    PKCS8EncodedKeySpec pubKeySpec= new PKCS8EncodedKeySpec(encodedPrivateKey);
    KeyFactory keyFactory = KeyFactory.getInstance("DSA");
    PrivateKey key = keyFactory.generatePrivate(pubKeySpec);

    Signature dsa = Signature.getInstance("SHA1withDSA");
    dsa.initSign(key);
    dsa.update(serialBytes);
    return dsa.sign();
}

We are signing the license information using the private key to ensure that nobody else can generate licenses. The private key is read from a file into a byte array (encodedPrivateKey). We then convert the binary representation into an internal ASN.1 representation using the PKCS8EncodedKeySpec class from the java.security.spec package. The key representation is then converted into an instance of PrivateKey using the DSA key factory. With the key and the serial number bytes on hand, we obtain an instance of the secure hash with the DSA algorithm (SHA1withDSA), supply its parameters, and generate the digital signature using the sign() method. Running the license generator script licenseGenerator.bat for the chat.license configuration file in the distrib/conf directory produces the following output:

C:CovertJavain>licenseGenerator.bat -serial distrib/conf/chat.license
License information read:
   host=localhost
   ip=172.24.109.159
   expires=Sat Jan 01 00:00:00 EST 2005
Serial=[MC0CFBiEzKka0pnEQSlDyKxbHy+gE1+zAhUAlxPlWyAXcCDcoWSRY/Kk/xAkvTQ=]

We now need to copy the generated serial number and paste it as the value of the serial property in the chat.license file. The license generation is complete.

Verifying the License File

We have to enhance the Chat application to read the serial number and verify that the license parameters have not been tampered with. Because the serial number we use for Chat is actually a digital signature of the parameters, we need to code a method that uses the public key from the generated key pair to verify that signature. Let's add a method called verifySerialNumber() to the LicenseManager class that we coded earlier. To verify the digital signature generated using the private key, the method must use the public key. The license properties and the serial number were read from the license file in the LicenseManager constructor and stored in the member variables. The source code for verifySerialNumber() is shown in Listing 19.18.

Example 19.18. Verifying the Serial Number

public void verifySerialNumber(String keyFileName) throws Exception {
    String keyFilePath = this.home + File.separator + keyFileName;
    FileInputStream stream = new FileInputStream(keyFilePath);
    byte[] encodedPubKey = new byte[stream.available()];
    stream.read(encodedPubKey);
    X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encodedPubKey);
    KeyFactory keyFactory = KeyFactory.getInstance("DSA");
    PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);

    byte[] licenseData = getLicenseString().getBytes(CHARSET);
    String encodedSig = this.licenseProps.getProperty("serial");
    if (encodedSig == null || encodedSig.length() == 0)
        throw new InternalError("Serial number is missing");
    BASE64Decoder decoder = new BASE64Decoder();
    byte[] serialSig = decoder.decodeBuffer(encodedSig);

    Signature signature = Signature.getInstance("SHA1withDSA");
    signature.initVerify(publicKey);
    signature.update(licenseData);
    if (signature.verify(serialSig) == false)
        throw new InternalError("Invalid serial number");
}

The method first reads the contents of the public key file and uses the X509EncodedKeySpec class with the DSA key factory to convert the binary representation of the key into an instance of the PublicKey interface. Then the unified representation of the license parameters returned by getLicenseString() is converted to an array of bytes. The serial number is read as the value of the serial property from the license file and, if it is not missing, the number is decoded from base64 encoding using the BASE64Decoder class. An instance of the SHA1withDSA signing algorithm is obtained and supplied with the public key and the license data. Finally, a call to the verify() method of the signature algorithm is used to test whether the serial number data is a correct digital signature for the license data. If the verification fails, an exception is reported using InternalError.

To integrate the license verification with Chat, we need to add the invocation of verifySerialNumber() to ProtectedChat's main() method. Because we already have an instance of LicenseManager in main(), we just add the block of code shown in Listing 19.19.

Example 19.19. Invoking License Verification

public static void main(String[] args) throws Exception {
    ...
    // Check license information
    licenseManager.verifySerialNumber("conf/key_public.ser");
    if (licenseManager.isHostAllowed() == false)
        throw new Exception("Host is not allowed by the license");
    if (licenseManager.isLicenseExpired() == true)
        throw new Exception("The license is expired");
}

If the verifySerialNumber()method of LicenseManager does not throw an exception, the hostname and license expiration date verification are performed. The hostname verification is a simple string comparison between the name of the host that was read from the license file and the name of the host running Chat. The expiration date verification is an equally simple comparison between the current system date and the read license expiration date. Only if both verifications are successful do the commercial features of Chat become enabled. As long as the code in LicenseManager and ProtectedChat is not hacked, we have a pretty secure licensing mechanism.

An interesting approach to insert the licensing checks is to use bytecode instrumentation, described in Chapter 17, “Understanding and Tweaking Bytecode.” Rather than manually invoking the methods of LicenseManager throughout the application classes, a post-processor utility can be developed that decorates the key methods of the application with the license verification code. The utility would run after the source code is compiled but before it is put into a distribution JAR. The inserted bytecode would throw an exception or return an error if the license were invalid or if the feature were not allowed. This provides a clean separation between the application logic and licensing code.

Web Activation and License Registration

Using the licensing mechanism described in the previous section provides a great deal of protection against piracy. However, if the license verification is hacked, the proliferation of the compromised product can be hard to track, especially if it surfaces on the Internet. A good strategy is to duplicate the invocation of the license verification methods throughout the application code. In the sample code, the Chat application only instantiates and uses LicenseManager in ProtectedChat's main() method. For extra protection, you should call the same methods in the MainFrame or ChatServer code. Yet another measure of protection is activation and registration via the Web.

The idea behind Web registration is that each time the application is run, it connects to the vendor's Web site and checks whether the license information is still valid. This enables the vendor to track the number of installed versions and to turn off the licenses or builds that are known to be hacked. Establishing an online connection to the vendor provides additional benefits, such as the possibility of automatic updates and collection of usage statistics.

Online connection should not be viewed as the primary method of activation and registration, though. Products can be installed and run in a controlled, isolated environment behind company firewalls that completely block access to the Internet. Sending a customer's information to the vendor's Web site can also lead to privacy concerns.

Quick Quiz

1:

What are the differences between a message digest, encryption, and signing algorithms?

2:

What is the difference between symmetric and asymmetric algorithms?

3:

How would you protect the contents of an email sent via the Internet? Which JCE classes would you have to use?

4:

Which measures can be taken to protect the application content from hacking?

5:

We have used a message digest algorithm to compute the checksum of Chat's distribution files. Would it be more secure to use a symmetric or an asymmetric algorithm for the checksum? Why?

6:

What are the logical steps required to obtain a digital signature using a symmetric algorithm?

7:

What are the logical steps required to obtain a digital signature using an asymmetric algorithm?

In Brief

  • Cryptography provides a means of converting readable information into incomprehensible code that can be transmitted openly and then transformed back into its original form.

  • Encryption is the process of encoding readable information into code, and decryption is the process of extracting the readable information from the code.

  • Message digest algorithms do not modify the contents of the message. Rather, they produce a unique hash based on the message contents and a secret key.

  • Encryption/decryption algorithms hide sensitive information that can be intercepted by a third party. The message contents are modified using a secret key, producing output that is virtually impossible to convert back to the original contents without the key.

  • Signing refers to generating a relatively short digital signature based on arbitrarily sized data using a private key of an asymmetric algorithm. The signature is produced by the sender and is transmitted with the message. Authenticity is ensured through the use of asymmetric algorithms in which only the sender has the private key.

  • Java Cryptography Architecture (JCA) provides a complete and robust implementation of cryptography services and algorithms.

  • Encryption, message digests, and signing algorithms can be used to secure the communication between the layers of a distributed application.

  • Most security algorithms require parameters such as the password or keys. They typically operate on binary data.

  • After bytecode obfuscation, ensuring the integrity of distribution files is the most important measure that protects the application from hacking.

  • The most effective way to implement licensing that unlocks commercial features is to provide a text-based license file with a digital signature produced by an asymmetric algorithm.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.48.244