|
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.
|