Home Page
Blue Dust Blog
Projects
Quit!
Discussions
X Seeks Y
Links
About Us!
Quit!

Secure Programming

Game code is usually isolated from the real world. If we can’t make something happen we change it, remove it, and generally mess about with it until we can. This gives us complete control over the game, and all the parameters within. When the game takes data from an untrusted source these parameters are tainted, and may have been maliciously altered to give a player extra damage points, crash an opponents computer, or even provide a back door to allow the installation of Linux (as happened with 007:Agent Under Fire)! This programmer-oriented article details a number of code examples that are fundamentally insecure, and how to fix them. It also details some useful tools to ensure secure programming techniques.

The Problems

                 

Most software insecurity stems from a single premise; what you thought could never happen – happens. This might be as simple as an out of range parameter, or as complex as a well-crafted buffer overrun (see BOXOUT). This introduces a snowball effect that corrupts the game in some way, causing distorted results that can then be used for various nefarious purposes. When playing a network game a cracker could introduce an unfair advantage that, although unsocialable, might not generally be considered harmful. However, such behaviour can cause genuine (paying) customers to switch off because of the perceived difficultly. Furthermore, with sellers on eBay advertising Everquest gold coins for real-world currency (at an exchange rate that betters the dollar), providing any cheat with the means to produce game resources artificially might be construed as fraud… with insecure code aiding and abetting!

Even the single player game is not immune, although for different reasons. If a cheat crashes or corrupts their game, no one is going to care about the technical support query. But when that user finds a way to run Linux, other people start noticing. Sure, the sales for your game will increase initially, but with the potential legal issues surrounding it, and the subsequent negative publicity generated by the crack (since the only comments Google and his dog will remember about your game concerns it “being good for installing Linux,”) will ultimately lead to more harm than good.

Tainted Love

Such corruption can come from any external input source and must therefore be considered tainted, and potentially insecure. This is the start of the ‘taint chain’, and extends from the source material through each piece of code that does not validate its input. Since this can be a long chain, it is best to check all data when it’s first read because if you receive tainted data, you should never pass it on.

The most common secure programming technique to employ is validation. And lots of it. Every parameter must be checked using the typical ideas of,

* Length

* Size

* Range

* Integrity

When dealing with the obvious case, strings, be sure to avoid the dangerous functions (presented in the table below) because simple code like this,

char filename[20];

 

strcpy(filename, "gamedata/");

strcat(filename, levelname);

will corrupt the stack if the level name is too long. Although your level names will be short enough, a cracker will not be so considerate and may send filenames with several hundred characters in order to smash the stack and execute arbitrary code. See the BOXOUT for more information.

The above strcat call should be replaced with,

strncat(filename, sizeof(filename)-1, levelname);

The –1 is crucially important here since strncat doesn’t guarantee that the string will terminate will a NUL. So, if the level name was exactly 11 characters the last letter would be stored in filename[19] and there would be no room to hold the terminating zero. This has the potential of causing further problems. For example,

pLevelCopy = malloc(strlen(filename));

Here, the innocuous strlen function would not know when to stop counting, and the machine would allocate much more memory than intended. The ‘Function Replacement’ table below details similarly insecure functions.

Home on the Range

Checking the range of simple integers can be surprising beneficial. Imagine a real-time strategy game where N units of gold are removed from the map whenever they are mined. What happens if the value of N is surreptitiously reduced to one. Or zero. Or a negative number. How will the gameplay mechanics cope.

One typical validation problem involves filenames. These should be checked letter-by-letter for invalid characters like the dot, colon, forward slash and back slash. All filenames should be parsed to remove such characters, and any attempts to use the parent directory (..) or similarly invalid paths should be ignored. The correct approach is to accept only those characters you consider valid; and not to discard any you consider invalid. Cracker ingenuity (or user stupidity) will always find another invalid test case.

If you are supporting escaped characters in your string, for example \n, then remember to parse them carefully, and only parse for escape characters once. Otherwise, a perfect valid string could include escape sequences which are themselves escaped, such as \\0. Here the \\ reduces to \ on the first pass, and \0 on the second. This introduces a NUL terminator into the string prematurely. It is not too difficult to build code sequences in this fashion that can bypass most parsers.

Also, when eliminating the double dot (..) from filenames, consider what happens if three (…) or four (….) dots are used. If you utilize complex textual patterns, then consider creating a regular expression to validate the input and employ existing library code, such as Phil Hazel’s wonderful pcreg, to implement it.

One specific instance of character escaping that needs to be considered is the format specifier %s, and its use within sprintf. Under normal circumstances there is nothing wrong with,

sprintf(string, szFormatString);

However, secure programming does not come under the banner of ‘normal circumstances’. If the format string was, in fact, the level name loaded from a memory card, or network packet, the tainted data could include format specifics such as %d or %s. This can result in buffer overruns, long strings, or unchecked parameters added into the format string. The code above should be replaced with the more secure,

sprintf(string, "%s", szFormatString);

Additionally, you should study the resultant name as a whole for suspicious combinations -- again, accept valid combinations, as opposed to discarding invalid ones. If you’re running under Windows it might be possible to initiate a game with the name “com1:”. How is that handled? Perhaps the operating system transparently handles alternate protocols, so a file at http://somedomain.com/dodgy.lev is loaded without the users knowledge? Façade-based web sites have captured credit card details for years, it’s probably not long before insecure games instigate something similar.

Finally, when validating characters be aware than there are numbers above 127. The signed/unsigned problem occurs here with chars, so the code might treat it as –128 or 128. This provides a simple backdoor which can avoid the character validation routines mentioned above.

Remember in all these cases that anything on the local machine can be compromised. While it’s true that the executable can still be hacked to remove validation checks, the amount of effort required to do so will take sufficiently long that it becomes inconsequential, especially if there are a lot of them. When running a console game, the effort of placing a new executable on a bootable disc is often quite immense, but still possible. This becomes even easier when a game downloads its own updates, as was the case with Phantasy Star Online.

File Handling

As we’ve already seen, being able to load arbitrary files is a minefield of problems, but if you’re using a mounting filesystem (and most cross-platform games will be) you can incorporate a lot of security into the filename parser at the low level. In this way, the filesystem can mount all the game data files from the physical CD-ROM into a virtual root directory. The filename can then be interpreted, relative to this virtual root, so that no code knows about the physical CD-ROM itself, or its directory structure. This makes it much more difficult to load inappropriate files into memory. For those interested in prior art, take the time to study a chroot jail in Linux.

You can eliminate the file loading problem by avoiding relative pathnames altogether. Instead, a file load of “/gamedata/level/1/level.lev” will be replaced with individual calls to ChangeCurrentDirectory, and a single load of “level.lev”. In this way, all non-alphanumeric characters will be flagged as suspicious and the appropriate action can be taken. Those actions might involve loading a generic level with a hardcoded name or, preferably, returning to the main menu.

The Tools

When dealing with problems of the C language it is usual to employ tools. The compiler provides our first line of defense by issuing warnings for the signed/unsigned problem, along with other unsafe type conversions. For stricter checking, tools such as Lint (as either PC-Lint by Gimpel, or the free variant, Splint) are available which will understand the semantics of your code. This catches problems where, for example, the switch statement omits potential cases, and provides an easy way to avoid validation checks. (Developers can eliminate these problems by using enumerations instead of integers to specify their cases, and using default to handle the error scenario.) Note that these tools should be used to find general development issues, and are not limited to secure programming.

From a purely C language perspective, an intelligent ‘find’ that searches for the forbidden functions (as detailed below) could be used. One such tool is called Flawfinder, by Mr. Secure Programming himself, David Wheeler. This, along with his electronic book on the subject, is freely available at http://www.dwheeler.com. For those interested in non-free tools, ITS4 will check C and C++ code for various security problems. More information can be gleaned from the http://www.rstcorp.com/its4 website.

Closing Comments

If you detect a hint of paranoia in these words, you are correct. Most of the secure programming issues arise in the Windows and Unix worlds and very few cases are reported within games. But with on-line games becoming the norm, the potential for problems will grow. At which point secure programming should become the rule - not the exception.

TIPS: Top Five Tips

1. Unify your validation routines. If two pieces of code have different ideas of what constitutes ‘valid’ data you’ll suffer the “Deputy problem” and be open to attacks as tainted data passes through different routes within the game.

2. Remove all debugging code from your game. This includes cheat screens, special key combinations, and any magic code activated with argument parsing through argc and argv. The latter can be compromised through simple custom software, similar in design to magazine coverdisk loaders, to pass bogus strings. This is an easy way of triggering buffer overruns on some platforms. Remember also that the problems with sprintf can also occur with printf or similarly prototyped trace functions.

3. Nothing is private. It is trivial to set up a home network to monitor network packets, so sending clear text passwords is always a bad move. The sign-on procedure, which only occurs once per game, can use very strong encryption, while the more frequent messages can use a fairly weak variety. You can then use the high strength methods to dictate changes in the lower strength encryption keys in total secrecy, thereby fooling any network snoopers. If you must hold a clear text password in memory then memset the memory immediately afterward it’s been used. This gives the smallest possible window of opportunity for memory walkers to recover anything useful about your servers.

4. Any code or data on the users own machine can be compromised and should not be trusted. You will always need additional authentication.

5. Don’t get complacent. Crackers have more time, and more to gain, by corrupting and perverting their game data, and those of others. Just because the game is closed source, runs on a single-user console, or requires privately controlled servers, doesn’t mean you’re safe. You’re not!

BOXOUT: Buffer Overruns

Of all the security issues that continually appear, the buffer overrun (or stack smasher) is the most common. It appears in so often that it accounts for over half of all “real-world” security problems, according to the CERT.

A buffer overrun occurs when well-crafted data (usually a string) overflows the storage space allotted to it. When this storage is a local variable, it will overwrite the functions local data and its stack frame. Then, when the current function attempts to exit, it unwinds the (now corrupted) stack and executes any rogue code now on it.

The specific string required to trigger a buffer overrun will vary between games, and can be complex to create, and even harder to deploy. But it is now a well understood cracker discipline, and no longer confined to Unix systems, as the Agent Under Fire case study shows.

The reason for their prevalence over other attacks is due mostly to C’s inability to prevent against them in an easy-to-write fashion. Each of the standard string functions, like strcpy, strcat and sprintf, are all vulnerable to buffer overrun attacks and must be prevented. Even C++ is not completely beyond reproach! Just remember that std::string hides a simple char *, and is susceptible to exactly the same problems.

New technology involving non-executable stacks will either reduce or eliminate this problem. Microsoft supply the /GS switch to minimize it which initiates a StackGuard-like technology which places a watched value in front of the return address. This is known as the “canary defense.” However, secure programming means appealing to the lowest common denominator, so we must try to prevent buffer overruns on all platforms.

TABLE: Function Replacements

Original Function          More Secure Function

strcat                            strncat

strcpy                           strncpy

sprintf                           snprintf *

gets                              fgets

* This function is not part of the standard. Its behaviour can be mimicked by using the %. format specifier to Indicate the maximum size of the string, thus,

sprintf(string, "%.*s", sizeof(string)-1, levelname);

Again, note the –1, and be warned that string in this example must be an array – not a pointer.

The Author

Steven Goodwin has been employed in the games industry for the last twelve years, his most recent bankrupted company being Computer Artworks where he worked as a Senior Programmer in the Core Technologies group. He can be reached at goodwin_steven at hotmail dot com.