Validating Your Code


At one time, the code you wrote stayed on a local machine or a network, so there was little chance that someone could get a copy of it—much less change it. The probability that you would suffer a major security problem due to the code alone was relatively small. Any code you wrote that also included reasonable protection from user mistakes and malicious tampering was safe.

However, now your code is accessible through Web services or through sharing with partners. In addition, .NET has certain weaknesses that are less inherent in native code. For example, it’s very easy to disassemble .NET code into a text file that anyone with reasonable programming skills can modify. For these and many other reasons, it’s no longer safe to assume that the code you write and distribute today is going to be safe tomorrow.

Chapter 4 covered the issue of code validation to an extent. The Signed Client example in Chapter 4 (see the “Writing the StrongNameIdentityPermission Class Code” section) shows one way to verify the identity of the caller. This verification process entails checking the validity of the code. A change in the code would cause issues when Common Language Runtime (CLR) tried to run it. In general, the check in this chapter alone is enough to provide satisfactory security for many needs. However, some applications need better security than the example in Chapter 4 provides.

The following sections discuss issues you should consider for code validation when security is at least as important as performance. For example, if you’re developing any kind of application that handles money, you need to validate the code to ensure that even small or odd bits of tampering haven’t occurred.

Checking the Intermediate Language (IL) Code

The first question you need answered is whether you can assume that your signed code is safe—the answer is no. Although you can sign your code and make it very tough for someone to misrepresent you or your organization, the fact remains that anyone with the .NET Framework installed on their system has the tools required to view and modify your code (not that it’s an easy task).

Let’s look at a simple example. (You can find this example in the \Chapter 06\C#\ SimpleSigned or \Chapter 06\VB\SimpleSigned folder of the source code located on the Sybex Web site.) The code of interest is the btnTest_Click event handler shown here.

private void btnTest_Click(object sender, System.EventArgs e) {    // Display a message box.    MessageBox.Show(“Hello”,                    “Message”,                    MessageBoxButtons.OK,                    MessageBoxIcon.Information); }

This simple example is signed with the MyKey key used for many of the other examples in the book. To open this file, create a command prompt, type ILDASM SimpleSigned.EXE, and press Enter. Figure 6.1 shows the disassembly of this signed file. Notice that signing this assembly doesn’t prevent you from reading it. You can learn anything you want about this assembly and .NET does nothing to stop you.

click to expand
Figure 6.1: Security information in a .NET IL file is easily readable.

There are many pieces of interesting information in this file, but the piece to focus on now is the MANIFEST entry displayed near the top of Figure 6.1. Double-click this entry. Look near the bottom of the file that opens and you’ll see information similar to the information shown in Figure 6.2.

click to expand
Figure 6.2: Reading what should be protected information is excessively easy in .NET.

From previous chapters you know that Microsoft provides tools for extracting at least some of this information from the manifest. However, few developers realize that all this information is so easy to access using a single utility. It would appear that your code is completely open to abuse and misuse. Even if you include public key checks and perform other types of validation, the fact that your code is so easy to read makes it unlikely that any security methodology will ensure complete code security. Native code compilation has a definite advantage here because the compiler can obfuscate the code sequence and the use of machine instructions makes the information nearly unreadable. (As a connoisseur of disassemblers, I can tell you that nothing is certain even with native code.)

Validating the Standard Check

CLR doesn’t leave your code completely helpless if you sign it. The checks it performs will detect a moderate level of tampering from an inexperienced .NET developer. It’s easy to check out this part of your protection package. Begin by creating a source code file. Type ILDASM /Source /Out=SimpleSigned.IL SimpleSigned.EXE at the command prompt, press Enter, and you receive text and resource files that define the example.

Make a simple change to the SimpleSigned.IL file. For example, you could change the word Hello to Goodbye, as shown in Figure 6.3. Notice that this is the IL version of the btnTest_Click event handler. You may find it interesting to look through this file. Many developers are unaware that their comments appear in the IL file, which can make for interesting reading.

Note

Some developers might consider the problem of comments in an IL file as an argument for removing comments from production level code, but the cure would be worse than the disease. Uncommented code causes more than a few problems and some companies even hire consultants to determine whether to salvage uncommented code or start from scratch. Removing comments from your code isn’t a good security policy.

click to expand
Figure 6.3: Creating a disassembly of a .NET application lets you modify the code.

Once you make a simple change, you need to compile the program back into an executable. At the command prompt, type ILASM /EXE /Resource=SimpleSigned.RES /Output=SimpleSignedMod.EXE SimpleSigned.IL and press Enter. The new file, SimpleSignedMod.EXE, still uses the original key as a signature. When you run it, you receive the error message shown in Figure 6.4.

click to expand
Figure 6.4: Casual crackers are unlikely to remain undetected during the standard CLR checks.

This part of the example demonstrates that CLR does, in fact, detect modifications to your signed executable (your unsigned executable is completely helpless). Note that you can dismiss the debugging dialog that appears after you click OK at the security message in Figure 6.4.

Circumventing and Fixing the Standard Check

What happens, though, if someone signs the new version of the file with their key using the ILASM command? At the command prompt, type ILASM /EXE /Resource=SimpleSigned.RES /Output=SimpleSignedMod2.EXE /Key=MyKey2 SimpleSigned.IL and press Enter. The ILASM command creates a new version of the program, just as it did before.

Tip

The exploit demonstrated in this section becomes much harder if you use a digital signature issued by a third party Certificate Authority (CA) because a cracker can’t simply substitute one key for another without notice. However, even using this method means that you need to alert users who are going to notice that the signature is no longer the same. Experience shows that most users simply aren’t that observant.

Run the resulting program. Suddenly, there’s no error message. Again, your code is completely helpless because CLR sees that the code is signed using a legitimate key—one supplied by the cracker. Unless you incorporate an internal check, there’s no hope of catching this problem. Now you see the benefit of the example in Chapter 4—at least no one can access your components unless they change them as well.

You can perform several checks to ensure that no one has changed your code. These checks won’t stop everyone, but they do add another security layer. Listing 6.1 shows the constructor for an improved version of the SimpleSigned program. (You can find this example in the \Chapter 06\C#\SimpleSigned2 or \Chapter 06\VB\SimpleSigned2 folder of the source code located on the Sybex Web site.)

Listing 6.1 Adding Security Checks to a Client

start example
public frmMain() {    StrongNamePublicKeyBlob       PublicKeyBlob; // Public Key Check    Assembly                      Asm;           // Current assembly.    Evidence                      EV;            // Assembly evidence.    StrongNameMembershipCondition StrongMember;  // Strong Member Check.    IEnumerator                   MyEnum;        // Security enumerator.    HashMembershipCondition       HashMember;    // Hash Member Check.    Hash                          ClientHash;    // Current client hash.    RegistryKey                   AppKey;        // Registry entries.      // Required for Windows Form Designer support    InitializeComponent();    // Create an array for the public key.    Byte[]   PublicKey = {       0, 36, 0, 0, 4, 128, 0, 0, 148, 0, 0, 0, 6, 2, 0, 0, 0, 36,       0, 0, 82, 83, 65, 49, 0, 4, 0, 0, 1, 0, 1, 0, 83, 12, 9,       107, 146, 83, 124, 20, 21, 86, 251, 134, 236, 238, 161,       253, 206, 142, 35, 243, 186, 79, 38, 30, 178, 10, 4, 92,       204, 156, 54, 242, 202, 193, 93, 134, 119, 0, 210, 166,       172, 82, 8, 75, 91, 111, 21, 220, 196, 237, 136, 41, 185,       196, 70, 179, 108, 206, 239, 254, 176, 196, 78, 78, 116,       129, 221, 119, 192, 87, 171, 167, 158, 74, 188, 21, 48, 207,       226, 106, 63, 227, 44, 243, 119, 5, 40, 155, 247, 54, 207,       18, 245, 89, 232, 128, 75, 14, 59, 120, 9, 68, 250, 206,       151, 24, 43, 168, 70, 116, 218, 59, 227, 28, 80, 85, 61,       175, 161, 70, 124, 19, 251, 129, 10, 213, 6, 223 };    // Create a public key blob using the public key.    PublicKeyBlob = new StrongNamePublicKeyBlob(PublicKey);    // Create the strong name membership condition.    StrongMember =       new StrongNameMembershipCondition(PublicKeyBlob, null, null);    // Get the calling assembly.    Asm = Assembly.GetExecutingAssembly();    // Get the evidence from the assembly.    EV = Asm.Evidence;    // Check the strong name membership.    if (!StrongMember.Check(EV))    {       // Throw an exception.       throw(new SecurityException("Failed Internal Security Check"));    }    // Get the hash value for this client.    MyEnum = EV.GetHostEnumerator();    ClientHash = null;    while (MyEnum.MoveNext())       if (MyEnum.Current.GetType() == typeof(Hash))          ClientHash = (Hash)MyEnum.Current;    // Check for the Hash value.    AppKey = Registry.LocalMachine.OpenSubKey(@"Software\SimpleSigned");    // The registry key does exist.    if (AppKey == null)    {       // Create the registry key.       AppKey =          Registry.LocalMachine.CreateSubKey(@"Software\SimpleSigned");       // Set the registry value.       AppKey.SetValue("HashValue", ClientHash.MD5);    }    else    {       // Create the Hash membership condition.       Byte[] HashValue = (Byte[])AppKey.GetValue("HashValue");       HashMember =          new HashMembershipCondition(new MD5CryptoServiceProvider(),                                      HashValue);       // Verify the hash value hasn’t changed.       if (!HashMember.Check(EV))          throw(new SecurityException("Failed Internal Hash Check"));    } }
end example

This code may look a little bizarre, but it’s quite effective in securing your application. The code performs two checks. First, it looks for the public key assigned to the application. This check overcomes the problem of someone using another key to sign your application. Second, it looks at the hash value of the program. Even if someone should decide to replace your public key value with the new ones for their key, replicating the hash value is extremely hard. In addition, this value isn’t stored in plain view in the disassembled code.

The strong name check has appeared in several other examples in various forms. This is yet another form of that very useful check. The code creates a StrongNameMembershipCondition object and then uses the Check() of that object to verify a standard strong name value against the strong name value in the current client.

The hash check begins with the code obtaining the hash value of the application, as it currently exists. If a cracker has changed the code in any way, the current hash value won’t match a stored hash value. However, there’s a problem. You can’t extract the hash value easily and it doesn’t appear in the disassembled code. Finding the hash value so you have something to use for comparison purposes could be a bit problematic.

The example solves this problem by using the registry. The registry solution actually works very well if you also set policies that restrict access to that key and its values. Even if you don’t secure the key, the probability that someone will know where to look in the registry for your application’s key is small. For the purpose of this example, the registry check can take one of two courses. If the registry key doesn’t exist, the code creates it and adds a value. On the other hand, if the key does exist, the code extracts the known good hash value from it. If this were a production application, you would add the key to the registry during installation and raise an error if the application couldn’t find it. The code creates a HashMembershipCondition object and uses the Check() method to validate the hash key.

I would love to say that this technique is foolproof, but only a fool would believe that statement. This two-phase check greatly increases the security of your program at a very modest cost in startup time. It doesn’t cost any performance once the program is running, nor does it cause the program to use any more resources (at least not once the Garbage Collector does its work). However, it’s possible that a determined cracker could still overcome both checks. All the cracker would need to do is add a new public key and create a new hash value for the registry. It’s possible, but more difficult than just making a change and recompiling the code.

As previously mentioned, multiple layers of security will slow a determined cracker down, but you can never stop a cracker completely.

Protecting Your Code with Dotfuscator

All of the problems mentioned so far have hinged on one factor, the ability of the cracker to read your code. This is a very big security problem because a cracker can analyze any security measures you take. There’s a strong third party market in programs that make your code unreadable using ILDASM. In fact, Microsoft has recognized that code readability is a problem, so they included Dotfuscator Community Edition with Visual Studio .NET 2003. If you’re using an older version of Visual Studio .NET, you can download this product from http://www.preemptive.com/dotfuscator/.

You can use Dotfuscator directly from the command line, which means you can simply add it to the compile process for your program. However, it pays to use the GUI the first few times, so you better understand how the product works and discover which features you want to use for your application security needs. Figure 6.5 shows the initial Dotfuscator display.

click to expand
Figure 6.5: The initial Dotfuscator display shows that this product can perform a number of tasks.

As you can see, the product can do quite a bit—each task area is on a separate tab. The following steps show how to perform the essential task of making your code unreadable. (You can find this example in the \Chapter 06\Dotfuscator folder of the source code located on the Sybex Web site.)

  1. Select the Options tab and check the Verbose option. This option is important because it lets you see what Dotfuscator is doing and provides a better sense of the product’s utility. Notice that you can also set the product to keep output to a minimum using the Quiet option. Use the Investigate option if you want to see the product work without producing any output and the Library option if the modules you specify as input are part of a library of modules.

  2. Select the Trigger tab. The Professional Edition performs application optimization by only including methods in the output that the code calls in the application. The Community Edition only uses this tab as a means of selecting the assemblies that you want to work with. In both cases, type the name of the assemblies you want to work with or use the Browse button to find them.

  3. Select the Rename tab. You’ll see the display shown in Figure 6.6. (It’s interesting to compare the list shown in Figure 6.6 with the one shown in Figure 6.1—Dotfuscator does a great job of analyzing your code.) In general, Dotfuscator renames everything in a module. This could be a problem if you call a specific method from an unrelated program. Use this tab to select elements that you don’t want to rename. It’s important to remember that anything that you don’t rename provides crackers with valuable clues. The default setup of renaming everything works for most applications.

    click to expand
    Figure 6.6: Renaming all of the elements in your program makes it harder for crackers to locate specifics.

  4. Select the Build tab. Type entries in the Temporary Directory and Destination Directory fields. These fields define the temporary and permanent location of data for the selected program.

  5. Save the project using the File>Save command. Dotfuscator saves its configuration files as eXtensible Markup Language (XML), making them relatively easy to read. Figure 6.7 shows the configuration file for this example. You can use this information to define your own custom project files.

    click to expand
    Figure 6.7: Reading the GUI generated XML files can help you build your own custom versions.

  6. Click Build. Dotfuscator builds the program for you. The lower window shows the build progress. You can see the results of the build process on the Output tab shown in Figure 6.8.

    click to expand
    Figure 6.8: Building the application generates information on the Output tab and in the lower window.

Of course, the question is how well Dotfuscator does in making your code unreadable. The initial output that appears in Figure 6.9 looks good. ILDASM displays a program organization that doesn’t look much like the original in Figure 6.1. However, if someone is determined, they can still learn the details of your program. Dotfuscator does confuse things, but it’s nothing like trying to disassemble native code.

click to expand
Figure 6.9: Using Dotfuscator will make your code harder to read and interpret.

Unfortunately, some aspects of the program are still all too readable. For example, the MANIFEST entry hasn’t changed at all. You can still read the public key without any problem at all. The constructor is also easy to find. Dotfuscator makes subtle changes to the code that makes it more difficult to figure out what’s going on, but again, a determined cracker can learn what your code is doing.

In the end, Dotfuscator is another piece of the security you should use to protect your application, but you can’t count on it as the only piece. Although the product sounds promising and does some of what it promises, it’s not a complete solution. You still need to sign your code and use techniques such as the hashing method shown in the “Circumventing and Fixing the Standard Check” section. Even so, you must realize that someone who is determined to modify your code can probably do so, which means you must remain vigilant if you plan to make your system secure.

Creating a Security Deployment Package

The .NET Framework Configuration Tool helps you create a security deployment package that contains the settings needed to support your application. The advantage to using this method is that you can create a security policy on a test machine, verify that it works, and then deploy it to your entire organization using the resulting Microsoft Installer (MSI) file. Whenever the user logs in after deployment, Windows automatically installs the new policy, which means you have fewer security problems due to security setup issues.

The disadvantage to this method is that it’s level oriented. You have to install the entire level to the MSI file. This means you couldn’t develop the policy on a standard machine—one that could have policies you don’t want the user to have. The only way to use this method

effectively is to create the policy on a machine that has no other policies installed—preferably one with a fresh installation of Windows. The following steps tell how to use this technique from within the .NET Framework Configuration Tool. (You can find an example file in the \Chapter 06\MyPolicy.MSI file of the source code located on the Sybex Web site.)

  1. Highlight the Runtime Security Policy folder.

  2. Click Create Deployment Package. You’ll see a Deployment Package Wizard dialog box.

  3. Select the level you want to deploy (Enterprise, Machine, or User).

  4. Type the location you want to use for the MSI file.

  5. Click Next. You’ll see a completion dialog box.

  6. Click Finish. The wizard creates the deployment package for you.




.Net Development Security Solutions
.NET Development Security Solutions
ISBN: 0782142664
EAN: 2147483647
Year: 2003
Pages: 168

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net