Mea culpa. Ok, I screwed up. What follows is an annotated version of the blog entry I posted earlier today, annotations courtesy of Eric Lippert (the guy who actually wrote a good portion of VBScript). As Eric points out, I made a couple of glaring mistakes in my original posting. First, although the script I provided works, my explanation of how the script works was a bit off-kilter, to say the least. Eric has graciously corrected those mistakes, and I thank him for that. (Actually my first thought wasn’t to thank him, my first thought was, “Oh, yeah? Well, I’ll just take a look at his blog and point out all the mistakes he’s made.” Needless to say, I rather quickly determined I’d have to come up with a Plan B.)
Perhaps more important, Eric has also pointed out that I was a bit lah-de-dah in my approach to this. And he’s right: I was just trying to come up with something different to talk about (believe me, writing a blog on scripting isn’t as easy as you might think). My intention was to demonstrate the ability to generate random numbers/letters using VBScript, and I chose to use passwords as an example. That’s OK, I guess, but I should have made it clear that this was just a demonstration; if you’re running a bank Web site or something and you need to generate random passwords for real security purposes, VBScript is not exactly the best option (for reasons Eric explains). I didn’t exactly make that clear. I was actually thinking more in terms of maybe generating a temporary password that users were immediately required to change upon logging on; a pseudo-random password seemed a bit better than using the word password, as I know many people do. So why didn't I just say that? Well, let's just say, in the immortal words of Joe Theisman, that I'm no Norman Einstein, if you know what I mean.
Anyway, after drying my tears, I took the original entry and annotated it with Eric’s comments; I actually think it makes for a much more interesting read that way. In fact, maybe I should promise to write more entries that will require clarification, annotation, and outright correction; after all, that’s something I know is correct!
Thanks, again, Eric. (Although I’ll be watching you every step of the way, just waiting for you to make a mistake – ah, never mind. Even if you did make a mistake, I’d probably never catch it anyway.)
Here is the revised entry:
Microsoft is smack dab in the middle of the Puget Sound, home to both earthquakes and Bigfoot. (I’ve never actually seen a Bigfoot, but we’ve had a couple earthquakes in the past few years.) Because of that, we are constantly urged to keep emergency supplies – blankets, flashlights, bottled water, etc. – in our homes, our cars, our offices, anywhere we go. Now, I have to admit, I’m not very good at following those directions; we do have a really nice flashlight at home, but it’s kept upstairs in one of those closets I’m afraid to go into when the lights are on, let alone when the lights are off. If we ever do have a really big earthquake, or an invasion of Bigfoots, well ….
However, while I’m not very good at preparing for natural disasters, I am reasonably good at preparing for blog disasters. For example, when we first started this thing I wrote an entry on how to generate random passwords using VBScript. When I finished I thought to myself, “Big deal. Might as well just toss this one.” But then I remembered my emergency preparedness training and thought, “Wait a second, don’t throw that away. Someday there might be an earthquake or you might be too lazy to write something else, and then you can just use this instead.” I kinda of assumed I’d have this in my pocket for years, keeping it as a safety net. But here we are, two weeks into blogging, and I’m already using it up. But, what the heck: six months from now you’ll have forgotten all about this, and I can use it again. Now that’s being prepared.
Posted by Greg Stemp. One of the main reasons we started this blog was because I have the attention span of a hummingbird, and find it difficult to spend more than a few minutes at a task before I get bored and want to move on to something else. Wouldn’t it be better for me to become really good at one or two things than to be mediocre at 20 or 30 things? Sorry; I wasn’t paying attention; did you say something?
In addition to my restless nature, however, another reason we decided to do a blog was to give us a chance to address topics that we couldn’t really address elsewhere. Maybe these topics were too little to warrant an entire column or Webcast. Maybe we’d already covered them in some detail but now we wanted to add something we’d just learned. Or maybe they were interesting, but kind of fell out of the mainstream of system administration tasks.
Today’s entry fits the latter category: we’re going to show you how to use VBScript to generate random passwords. That’s potentially useful, but for once we aren’t really concerned with whether or not this helps you carry out the day-to-day chores of a system administrator; instead, it’s just kind of neat, and gives us a chance to explore one of the many facets of VBScript that we never get around to talking about. (If you’re dying to see information on how to calculate sines and cosines using VBScript, well, hey, one of these days ….)
So how do we generate random passwords using VBScript? Well, to begin with, we have to do this character-by-character; there’s no built-in VBScript command named GenerateRandomPassword. So let’s say we want a seven-character password. In that case, we’re going to have to randomly generate seven characters, and then string them all together into one password.
Of course, even that’s not entirely true: VBScript can only generate random numbers, it can’t randomly generate other characters (like, say, letters). So does that mean we’re limited to passwords composed only of numbers? Of course not. Instead, we’re gonna cheat here. As you probably know, anything you can type on a computer – letters, numbers, punctuation marks, tabs, spaces, whatever – has a corresponding ANSI value. For example, in the world of ANSI the lowercase a is a 97; the uppercase A is a 65. Pretty much all the characters used in standard English have ANSI values between 33 (!) and 126 (~).
How does that help us cheat? Well, VBScript has a function – Chr – that can convert an ASCII value to a standard character. For example, maybe you don’t believe me that the lowercase a is a 97 in ANSI. Well, try running this script, then, and see for yourself:
See, told ya.
So how does that help us? Well, suppose we generate a random number between 33 and 126, and let’s suppose our random number generator returns a 48. We can then use the Chr function to turn this random number into a real, live character (in this case, the numeral 0). If we run the random number generator seven times, we’ll generate seven characters. If we string all those characters together – voila! a random password.
(How random are these passwords? Well, I don’t know beyond, “Pretty random.” Just for the heck of, I put the little script I’m about to show you into a loop, ran it 10,000 times, and saved it to a text file. There were no duplicate passwords in that batch of 10,000. My next experiment will be to run the script one million times to see if I can recreate the works of Shakespeare before those million monkeys typing on a million typewriters do.)
That’s all pretty cool and works great. Let me warn you, however, that random number generation is weird (or at least it seems that way to me). With that in mind, don’t worry about trying to figure out why we need to do some of the things we need to do in this script. Just accept things as they are, and move on with your life.
Eric Lippert comment. Don't you think that's kind of a bad attitude to encourage towards a program designed to increase enterprise security? I tell people doing security programming the opposite -- to write programs that keep your enterprise secure you need to understand absolutely everything that you are doing at a deep level, because if you don't, you're going to screw it up.
First, let me show you the script, and then I’ll explain what’s going on in each section. (And, yes, if all you want to do is copy the script code and not have to read the rest of blog entry, then this is the time to start copying and pasting.)
Here’s the script:
intUpperLimit = 14
intLowerLimit = 7
intCharacters = Int(((intUpperLimit - intLowerLimit + 1) * Rnd) _
intUpperLimit = 126
intLowerLimit = 33
For i = 1 to intCharacters
intASCIIValue = Int(((intUpperLimit - intLowerLimit + 1) * Rnd) _
strPassword = strPassword & Chr(intASCIIValue)
We start off by setting the values of two variables, one to 14, one to 7. Why? Well, as I said before, we have to run the random number generator several times in order to get a password; that’s because it only generates one number (and thus one character) at a time. Thus we need to set up a loop that runs X number of times. If we wanted all our passwords to be the same length (say, 10 characters), we wouldn’t have to do this; we could just loop 10 times. But we want to have variable-length passwords, passwords that can be anywhere from 7 to 14 characters. And that’s where the 7 and the 14 come from.
After assigning values to the two variables, we call the Randomize statement. Why? Well, believe it or not, it’s possible to configure the random number generator so it returns the same number every time (in other words, a completely non-random number). Why would you want to do that? I have no idea.
Eric Lippert comment. There are lots of reasons. Here's one. Ever played FreeCell? You can type in a "game number" and play the same game over and over again. If you get stuck, for example, and want to come back and try it again tomorrow.
The game number is simply the seed to the RNG that generates the deck. You want to be able to generate the same deck every time, even though it is "random".
Another example Imagine a world where you couldn't reliably reproduce random sequences: You have a sort algorithm. The tester wonders how it behaves on random lists. So they generate a few million random lists, and uh oh, one of them bluescreened the machine. Bummer. I guess we'll never see THAT sequence of random numbers again…
There are many other situations in which you want a predictable sequence of pseudo-random numbers; those are just a couple.
But to prevent that from happening, you need to “seed” the random number generator each time you call it (that is, give it a different starting point).
Eric Lippert comment. No, you only need to call Randomize _once_, not once per call to Rnd. Once the system has been seeded, each random number generated acts as the seed for the NEXT call to Rnd. The RNG has been cleverly designed so that it does not go into short loops.
In fact, you MUST only call Randomize once. Doing so in a loop, as you are doing, makes the random number sequence LESS random, not MORE random. Why's that? Read on!
The Randomize function seeds the random number generator by passing it the current system time, a nifty little trick considering the fact that – in this dimension anyway – each moment in time is unique.
Eric Lippert comment. Now this is a crucial error, in several ways.
First, every moment in time is not unique. First off, the statement is trivially untrue, as the timer gives the number of seconds gone by today. The moment in time "exactly noon, 30 December 1996" has exactly the same timer value as "exactly noon" every other day.
Second, you have not taken the granularity into account. The system timer is only accurate to the nearest millisecond, which means that two times that differ by less than a millisecond often do not get unique numbers. But worse, VBScript rounds off the timer to a float, for some unknown reason. There are 86.4 million milliseconds in a day, but only 23 bits of mantissa in a float, and 2^23 is less than 8.4 million! So the timer can't possibly be accurate to more than ten milliseconds.
That means that if there are two calls to a timer-based Randomize in the same tenth of a second, the randomizer gets the same value.
Fortunately, we considered this possibility when we wrote the Randomize method. Actually what the Randomize method does is it computes a new seed based on 16 bits from the timer and the previous seed, where the Rnd method changes the seed. But even so, this is adding very little entropy to the mix. Calling Randomize frequently makes the sequence of random numbers not really much harder to guess. (Ironically, the faster the machine gets, the less entropy Randomize adds, as there is much higher likelihood that any two operations will hit the same 10 ms window.)
I would not recommend calling Randomize more than once per program. If you think you need to, to add more entropy, then you need to consider using a more buff RNG.
That brings us to the most important problem with your algorithm. An attacker who possesses one randomly generated password can deduce the other randomly generated passwords from it if this algorithm is used to batch-generate many passwords at once.
All the attacker needs to know is your algorithm and one of the passwords. As I mentioned before, there are fewer than 8.3 million possible seeds. Suppose the attacker gets password kWiJiBo. The attacker first writes a loop that resets the machine's timer to each of the 8.3 million possible seeds, and determines which ones return "k" as the first generated letter. Fewer than 1% of them will, so now there are only 80000 seeds to check to see which one return kW as the first two generated letters. That reduces the problem down to about 800, and pretty soon the attacker knows EXACTLY what the random number seed was at the time his password was generated. (Thanks to all your calls to Randomize, the attacker can also deduce what the system time was when his password was generated, which might be interesting information.)
The fact that you've thrown so many extra Randomize calls in there makes the attacker's job slightly harder, as there's now got to be a few adjustments to the cracker algorithm to take machine timings into account, but like I said, calling Randomize doesn't really add very much entropy to the mix. Any attacker who knows a little basic cryptography and has a fast machine could crack this system in a few hours, and they'd then know every password generated by the system.
Generating random passwords safely requires considerably more buff random number generation than we provide in VBScript. If you want to be generating random passwords, I recommend using the Crypto APIs. Their random number generators are "true random" in the sense that it is computationally infeasible to determine the next number given the previous one. The pseudo-random number generator is not suitable for any security application, any video game where there's money on the line, etc.
Ok, brace yourselves, because now comes one of the strangest looking lines of code you’ll ever see in a script:
What in the world is going on here? Believe it or not, what we’re doing here is generating a random number (notice the Rnd function buried deep within all those parentheses), and then assigning that random number to the variable intCharacters (this is the number of characters we’re going to have in our password). We’re also using the Int function to convert the randomly-generated number to an integer; if we get a value like 99.303, we want to chop the decimal places off and end up with plain old 99.
The heart and soul of our line of code is shown in boldface below; this is where the random number actually gets generated:
Weird as this looks, it’s a mathematical function that will generate random numbers between 14 (intUpperLimit) and 7 (intLowerLimit). In other words (well, in other numbers):
((14 – 7 + 1) * Rnd) + 14
Yes, we know it looks crazy. Like we said, the best thing to do is not worry about it, but just use the boilerplate code as provided. For example, what if you want to generate random numbers between 1 and 100? Then use this code:
intUpperLimit = 100
intLowerLimit = 1
Just replace intUpperLimit and intLowerLimit as needed, and try not to lose any sleep over all those parentheses. (Or, I suppose you could try counting those parentheses as opposed to counting sheep. Whatever works best for you.) In fact, in our script, we’re going to set the value of these two variables, and then a few lines later we’re going to change the upper value to 126 and the lower value to 33. Why? Because now we want to generate random numbers between 33 and 126, numbers that correspond to our desired set of ANSI codes.
After calling the Rnd function, we’ll have the number of characters in our password; that might be 7, it might be 9, it might be 14. This means we can set up a loop to actually generate the password (remember, we need to call the random number function once for each character in the password). That explains this line of code:
In other words, if our random number generator decided we’d have 11 characters in our password, intCharacters will be equal to 11,. As far as WSH is concerned, our For Loop would actually read like this:
For i = 1 to 11
Believe it or not, the rest is even easier. First, we call the Randomize function and we generate a random number between 33 and 126, a number we assign to the variable intASCIIValue. And then we have this line of code:
strPassword = strPassword & Chr(intASCIIValue)
What’s going on here? Remember, we’re using the Chr function to get the character equivalent of our random number; if we randomly generated a 97, then Chr will covert that to a lowercase a. We then take that lowercase a and tack it onto strPassword, a variable we are building, one character at a time, until we get our password. At this point in time, our password consists of a single character: a. Suppose on our next trip through the loop we randomly generate a 98. Chr converts that to a lowercase b, and we tack that on to strPassword; now our password string is ab. We just repeat that process X number and times, and we end up with something like this: ab5&hy1@WE.
Well, of course you could have gotten that same gibberish just by randomly pounding on your keyboard. But the point is, if you need gibberish like this, there’s no need for you to sit there and type it in; let a script automate the creation of gibberish for you. (A process similar to what many Hollywood screenwriters apparently do.)
Will you ever need to generate random numbers, let alone random passwords? Beat us. But, hey, what’s wrong with learning for the sake of learning? Remember, not everything in life has to be practical and pragmatic. (A good thing, too, seeing as how I’m neither.)
Eric Lippert comment. For "one off" password generation, your algorithm is just fine. But why would you write a script for a one-off? People write scripts for things they have to do over and over again, which is what makes me nervous about this example.
And of course, in the final analysis, the strength of a password needs to be correlated to the value of the property protected by it. Would this be a good algorithm for generating hotmail passwords? Actually, yeah, it's not that bad. What's the worst that can happen? Even if the attacker could figure out the password sequence, there are so many people signing up for hotmail every day, correlating the password with the user would be rather tricky.
But enterprise administrators might generate a whole pile of passwords with an algorithm like this one for a bunch of alphabetically-sorted users. In that situation, an evil user could figure out his coworker's passwords, and that might be quite bad.