emember the ILOVEYOU virus which inspired such favorites as Mother's Day, Very Funny, and "Anna Kournikova"? These viruses spread rapidly and did a great deal of damage for two reasons. First, they employed the easy-to-use architecture of WindowsÂ® Script Host (WSH), which was designed to be a powerful tool for system administrators who need to programmatically access the file system, e-mail, and so on. These viruses exploited that easy access. Second, the writers of ILOVEYOU and friends used a little knowledge of human nature to convince hundreds of thousands of curious peopleâ€"even people who knew betterâ€"to click Open it on the familiar dialog shown in Figure 1.
Figure 1 File Download Warning
The dialog asks users to be certain that the file in question comes from a trustworthy source. That's fine for executables because they can be signed, but until now there was no way to be sure that script files (.VBS, .JS, .WSF files, and so on) were coming from a trustworthy source, leaving users to just make the dialog go away.
One way to circumvent this problem is to reduce the power of tools or applications until they are incapable of causing harm by loading or running scripts, but that has undesirable side-effects. It's much better to let users verify trust and for system administrators to be able to make trust decisions for users.
In this article I'll describe how scripts can be signed in the same way you have been able to sign executables and how WSH works with this system.
So Who Can You Trust?
Whether I trust or distrust you, the government, or Microsoft Corporation is based on my beliefs. No technical system can instill trust. But what it can provide is evidence that a given script file or application was written by the individual who claims to have authored it, allowing you to make your trust decisions on the basis of what you know about the author without the fear that someone is masquerading as a trusted individual. A system of credentials and certifying authorities is necessary to promote this trust.
For example, let's say the Department of Motor Vehicles is the certifying authority (CA) that issues certificatesâ€"in this case drivers' licensesâ€"that may be used to verify identity, age, and driving privileges. It would be rather inconvenient if you had to give someone a driver's test every time you needed to determine if he could drive. Most people trust the DMV to have validated the driver. If you trust the DMV, you can accept a drivers' license as proof of identity and driving privileges.
Similarly, there are CAs (such as Verisign) that issue certificates that not only verify identity but also assert that the credentialed individual writes trustworthy code. Every machine running Windows contains a list of trusted rootsâ€"Verisign, for instance, might be a trusted root. If a particular CA is trusted, then the certified software publishers of that CA are among your trusted publishers.
Cryptographically Verifying Trust
Given that you trust a CA to determine who is trustworthy, how can identity be verified cryptographically? The foundation of the system is the public-key cryptosystem. In conventional cryptosystems, the same key encrypts and decrypts the message. (Here I use the term "message" to describe all the code, files, and data.) This means that the key must be transmitted by secure means so that only the intended recipient can decode the message transmitted over an insecure channel.
If you had a secure channel, you wouldn't need keys at all, because there would be no chance of interception. But, what if you don't have a secure method of sending information? To address this issue, public-key cryptosystems have a pair of keys. One of the keys is made public and the other is kept private. A message encrypted with one key can only be decrypted by the other key of the pair. Therefore, simply possessing the encrypting key is not enough to allow you to decrypt the message. This way a secret message may be encrypted by the sole possessor of the private key, then sent over an insecure channel to the recipient, who can decrypt it with the sender's public key.
Code signing works on the principle that if a message is decryptable with the public key, it must have been encrypted with the private key. Because there is only one owner of the private key, being able to decrypt a signed script with a public key is good evidence that the owner of the private key wrote that script. If that owner is trusted by the recipient, then this is good evidence that the script is safe to run. It is nearly impossible to forge a private key, and since public keys aren't a secret, there's no value in stealing them.
However, there are several reasons why it is inconvenient to encrypt an entire message. First, public key cryptosystems are very slow when encrypting large messages. Second, while you will receive the signatureâ€"the encrypted script, the information about how to decrypt it, and the information about the certifying authorityâ€"it would be nice to have the text-based script code too, so that you don't have to decrypt the script to run it. But sending both the signature and the script would double the length of the message if the signature contained the entire encrypted script. For this reason, the script isn't actually signed; rather, a hash is signed.
A hash algorithm is a function that is applied to a script, returning a large numberâ€"usually 128 bits or soâ€"called the hash. This 128-bit hash number is signed with the private key, then the key and the certificate are added to the text in the script file.
Therefore, to sign a script, the following operations are performed automatically behind the scenes:
- Obtain a certificate containing a private key from a certification authority.
- Remove any existing signature from the script text.
- Hash the script.
- Sign the hash by encrypting it with the private key.
- Convert the hash and the certificateâ€"minus the private keyâ€"into a script comment and append it to the script.
To verify trust, the opposite steps are performed.
- Extract the certificate and signed hash from the script text.
- Verify that the certificate was signed by a trusted root, and hence is a trusted certificate.
- Decrypt the signed hash with the public key in the certificate.
- Compare the decrypted hash with the hash of the received script text.
If any one of these four steps fails, trust cannot be verified. You can run the script, but at your own risk. If step one fails, then the script cannot be trusted because it is not signed by anyone. If step two fails, then you have verified that an untrusted individual wrote the script. If step three fails, then either something is wrong with the certificate or the signed hash has been altered somehow. And if step four fails, then someone has changed the text of the script after it was signed, possibly by adding dangerous code.
Dealing with Attacks
There are several attacks that theoretically could be made against cryptographically secured scripts. Suppose an evil hacker took your signed script, decrypted the hash with the public key, and wrote an evil script with the same hash. Your signature would then work just fine with that script. This attack is the major weakness of signing a hash rather than the whole message. Fortunately, the hashing algorithm used by the code-signing tool has a nice property: no one has ever managed to find two documents that hash to the same value, and not for lack of trying either. If you manage to find a document that hashes to the same value as a safe document, is a legal script file, and does something evil, you could give up the evil hacker lifestyle and join the math department of any university you want.
In a different kind of attack, someone could compromise the private key. In a perfect world, a private key would be a single-use entity, and a new key pair would be generated for each signature. Unfortunately, continually publishing new public keys is impractical, so private keys are actually stored and reused. Private keys are protected in several ways. First, CAs maintain a list of certificates that have been revokedâ€"either because the keys may have fallen into untrusted hands or because the CA mistakenly issued a key to an untrustworthy individual. (The operating system can be configured to periodically ask the root CAs for a list of revoked certificates.) Second, certificates are designed to expire after a certain number of years. That way, if someone is using brute-force techniques to crack the encryption and decode the private key by analyzing the public key, the certificate will become invalid before enough time has elapsed to crack the key. (The best systems take years of supercomputing to crack keys.)
Show Me the Script Already
The script signer generates the signature by base-64 encoding the binary data representing the certificate-plus-signed-hash obtained from WinTrust. (Base-64 encoding is a way of saving binary information in human-readable format. Every three bytes of binary information consume four characters in the encoded version.) That information is then formatted as a comment and added to the end of the script.
For example, consider the VBScript file in Figure 2. You'll note that the signature is pretty longâ€"a good two-dozen lines or so. The vast majority of that is the certificate information; the encrypted hash is tiny. Though this is long compared to the script, the hash and certificate sizes are constant, so a thousand-line script will not have a longer signature than a single-line script.
The text has been hashed, so any change to the script text or encrypted hash will result in a failure of the hash comparison. Similarly, the certificate has itself been signed with the CA's private key, so if anyone makes an attempt to change the certificate, it will stop the trust verification.
Figure 3 Untrustworthy Script
If you take a look at this script using the ChkTrust.EXE program from the WinTrust toolkit, you'll see that you can't trust this script (see Figure 3). It was issued by me, not a CA, and why should you trust me to issue my own certificate? If I showed you a driver's license from My Very Own Department of Motor Vehicles you wouldn't assume I could drive a bus, would you?
What about WSH?
The first rule of cryptography is: don't write your own cryptography. It is far better to defer to the experts than take the risk of introducing a compromising bug by rolling your own. Therefore, the latest version of WSH plugs into the WinTrust system that is in turn built on top of the Win32Â® CryptoAPI. WinTrust is a group of code-signing and certificate management tools that is documented in MSDNÂ®. Script can be signed or verified using all the same tools that you would use to sign any .EXE, .CAB, .DLL, or .OCX file. WinTrust does all the actual cryptographyâ€"WSH just provides the code to hash to the cryptographic engine and sticks the resulting signature into a comment block. (With the script signature manager installed, WinTrust tools such as ChkTrust.EXE and SignCode.EXE can be used on .VBS, .JS, and .WSF files.)
Then, by examining the following registry key, WSH will determine if it should verify trust before running code.
HKEY_CURRENT_USER\Software\Microsoft\Windows Script Host\TrustPolicy
(If that key is missing under HKEY_CURRENT_USER, then it looks for the same key under HKEY_LOCAL_MACHINE).
In the registry, the trust policy is set to one of three values: 0,1, or 2. A value of 0 means run untrusted scripts. A value of 1 means prompt the user if asked to run an untrusted script. Finally, a value of 2 means do not prompt the user and do not run untrusted scripts. To prevent users from changing these keys, thereby defeating the security system, a wise administrator will set access control lists (ACLs) on them.
If you are writing your own script host, it's easy enough for you to verify trust of a .VBS or .JS file the same way that WSH does. Just call the Win32 API WinVerifyTrust and the WSH signature manager will verify the file. Figure 4 shows a sample C++ routine.
Or, if you'd rather write script code to sign or verify scripts, there is now a Signer object shipping with WSH. Here's some sample JScriptÂ® code to sign a file.
var Signer = new ActiveXObject("Scripting.Signer");
var File = "c:\\myfile.vbs";
var Cert = "Jane Q. Programmer";
var Store = "my";
Signer.SignFile(File, Cert, Store);
The following sample VBScript code will verify a file:
Dim Signer, File, ShowUI, FileOK
Set Signer = CreateObject("Scripting.Signer")
File = "c:\newfile.wsf"
ShowUI = True
FileOK = Signer.VerifyFile(File, ShowUI)
If FileOK Then
WScript.Echo File & " is trusted."
WScript.Echo File & " is NOT trusted."
WSH Makes the Decisions
Right now the only script host that actually determines whether a script is trusted is the Windows Script Host. This functionality is in WSH, not in the script engines themselves, because the authority to trust a script does not really belong to the host. Some script hosts care deeply about trust and safety and some do not. Microsoft Internet Explorer, for example, ensures the security of script by greatly restricting what scripts can do. For Internet Explorer, trust is not an issueâ€"all scripts are considered untrusted because they came from the Web.
WSH provides signing and signature verification for VBScript and JScript engines when used by the Windows Script Host engine itself. Anything in a .WSF file used by the WSH script host (by anything I mean absolutely anythingâ€"third-party languages, whatever) can be verified, because it will be transmitted as text.
WSH does not provide signing or verification for VBScript and JScript used in any other script host (including Internet Explorer and Microsoft Internet Information Services, and so on ) because WSH is the only script host that treats a script as an executable, and because those other applications have specific security needs.
For third-party engines, WSH attempts to determine which signature manager to use based on the engine running the script. If you are using a third-party engine, the WinTrust system will attempt to find a signature manager based on the file extension. If there is a signature manager registered for that file extension, WinTrust will use it to verify any signature in that file.
Attacks Unique to WSH
Using the source tag in a WSH script can leave you vulnerable to attack. Suppose you have this blah.wsf file:
<script language="VBScript" src="library.vbs"/>
Call DoTheThing(1, 2, 3)
<!-- [ a valid signature block here ...
If WSH is verifying trust then it will verify that both blah.wsf and library.vbs contain valid signatures before it runs any code. Suppose, however, that you have two files, both named library.vbs, both signed by trusted authorities, and both with subroutines called DoTheThing that take three arguments. An attacker could potentially substitute one of those files for the other. The signature verifier verifies signatures of individual files but does not verify that a given collection of files is internally consistent, and that you're calling exactly what you think you are. You should be careful when using the source (src) tag with signed files.
Regardless of what your e-mail claims, not all your coworkers love you and not all jokes are very funny. Fortunately, tools exist to securely verify that a script author is trustworthy before a user opens potentially dangerous files. While these tools make the Web a safer place, they are really just the first step. There is still a long way to go towards making code signing and trust verification a seamless part of the development process.