The
threat of classic C exploits is reduced, but not eliminated, by using
high-level Objective-C APIs. This section discusses some best practices,
such as using NSString rather than legacy string operations like strcat
and strcpy to protect against buffer overflows. However, there are a
few more subtle ways that things can go wrong.
Buffer Overflows
The
buffer overflow is one of the oldest and most well-known exploitable
bugs in C. Although the iPhone has some built-in preventative measures
to prevent buffer overflow exploitation, exploits resulting in code
execution are still possible (see http://www.blackhat.com/presentations/bh-europe-09/Miller_Iozzo/BlackHat-Europe-2009-Miller-Iozzo-OSX-IPhone-Payloads-whitepaper.pdf).
At their most basic level, buffer overflows occur when data is written
into a fixed-size memory space, overflowing into the memory around the
destination buffer. This gives an attacker control over the contents of
process memory, potentially allowing for the insertion of hostile code.
Traditionally, C functions such as strcat() and strcpy() are the APIs
most often abused in this fashion. Sadly, these functions are still
sometimes used in iPhone applications today.
The simplest way for an
Objective-C programmer to avoid buffer overflows is to avoid manual
memory management entirely, and use Cocoa objects such as NSString for
string manipulation. If C-style string manipulation is necessary, the
strl family of functions should be used (see http://developer.apple.com/documentation/security/conceptual/SecureCodingGuide/Articles/BufferOverflows.html).
Integer Overflows
An “integer overflow”
occurs when a computed value is larger than the storage space it’s
assigned to. This often happens in expressions used to compute the
allocation size for an array of objects because the expression is of the
form object_size × object_count. Listing 1 shows an example of how to overflow an integer.
Listing 1. How to Overflow an Integer
int * x = malloc(sizeof (*x) * n); for (i = 0; i < n; i++) x[i] = 0;
|
If n is larger than 1 billion (when sizeof(int) is 4), the computed value of
will be larger than 4
billion and will result in a smaller value than intended. This means the
allocation size will be unexpectedly small. When the buffer is later
accessed, some reads and writes will be performed past the end of the
allocated length, even though they are within the expected limits of the
array.
It
is possible to detect these integer overflows either as they occur or
before they are allowed to occur by examining the result of the
multiplication or by examining the arguments. Listing 2 shows an example of how to detect an integer overflow.
Listing 2. Detecting an Integer Overflow
void *array_alloc(size_t count, size_t size) { if (0 == count || MAX_UINT / count > size) return (0);
return malloc(count * size); }
|
It’s worth noting at this
point that NSInteger will behave exactly the same way: It’s not even
actually an object, but simply an Objective-C way to say “int.”
Format String Attacks
Format string
vulnerabilities are caused when the programmer fails to specify how
user-supplied input should be formatted, thus allowing an attacker to
specify their own format string. Apple’s NSString class does not have
support for the “%n” format string, which allows for writing to the
stack of the running program. However, there is still the threat of
allowing an attacker to read from process memory or crash the program.
Listing 3 shows an example of passing user-supplied input to NSLog without using a proper format string.
Listing 3. No Format Specifier Used
int main(int argc, char *argv[]) {
NSString * test = @"%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x"; NSLog(test); NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; int retVal = UIApplicationMain(argc, argv, nil, nil);
[pool release]; return retVal; }
|
Running this results in the following:
[Session started at 2009-03-14 22:09:06 -0700.]
2009-03-14 22:09:08.874 DemoApp[2094:20b]
000070408fe0154b10000bffff00cbfffef842a4e1bfffef8cbfffef94bffff00c0
Whoops! Our
user-supplied string resulted in memory contents being printed out in
hexadecimal. Because we’re just logging this to the console, it isn’t
too big a deal. However, in an application where this output would be
exposed to a third party, we’d be in trouble. If we change our NSLog to
format the user-supplied input as an Objective-C object (using the “%”
format specifier), we can avoid this situation, as shown in Listing 4.
Listing 4. Proper Use of Format Strings
int main(int argc, char *argv[]) {
NSString * test = @"%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x"; NSLog(@"%@", test); NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; int retVal = UIApplicationMain(argc, argv, nil, nil); [pool release]; return retVal; }
|
NSLog makes for a good demo but
isn’t going to be used that often in a real iPhone app (given that
there’s no console to log to). Common NSString methods to watch out for
are stringByAppendingFormat, initWithFormat, stringWithFormat, and so
on.
One thing to remember is that
even when you’re using a method that emits NSString objects, you still
must specify a format string. As an example, say we have a utility class
that just takes an NSString and appends some user-supplied data:
+ (NSString*) formatStuff:(NSString*)myString
{
myString = [myString stringByAppendingString:userSuppliedString];
return myString
}
When calling this method, we use code like the following:
NSString myStuff = @"Here is my stuff.";
myStuff = [myStuff stringByAppendingFormat:[UtilityClass
formatStuff:unformattedStuff.text]];
Even though we’re both passing in
an NSString and receiving one in return, stringByAppendingFormat will
still parse any format string characters contained within that NSString.
The correct way to call this code would be as follows:
NSString myStuff = @"Here is my stuff.";
myStuff = [myStuff stringByAppendingFormat:@"%@", [UtilityClass
formatStuff:unformattedStuff.text]];
When regular C primitives are
used, format strings become an even more critical issue because the use
of the “%n” format can allow for code execution. If you can, stick with
NSString. Either way, remember that you, the programmer, must
explicitly define a format string.
Double-Frees
C and C++ applications can suffer
from double-free bugs, where a segment of memory is already freed from
use and an attempt is made to deallocate it again. Typically, this
occurs by an extra use of the free() function, after a previously freed
memory segment has been overwritten with attacker-supplied data. This
results in attacker control of process execution. In its most benign
form, this can simply result in a crash. Here’s an example:
if (i >= 0) {
...
free(mystuff);
}
...
free(mystuff);
In Objective-C, we can run
into a similar situation where an object allocated with an alloc is
freed via the release method when it already has a retain count of 0. An
example follows:
id myRedCar = [[[NSString alloc] init] autorelease];
...
[myRedCar release];
Here,
because myRedCar is autoreleased, it will be released after its first
reference. Hence, the explicit release has nothing to release. This is a
fairly common problem, especially when methods are used that return
autoreleased objects. Just follow the usual development advice: If you
create an object with an alloc, release it. And, of course, only release
once. As an extra precaution, you may wish to set your object to nil
after releasing it so that you can explicitly no longer send messages to
it.
Static Analysis
Most commercial static
analysis tools haven’t matured to detect Objective-C-specific flaws, but
simple free tools such as Flawfinder (http://dwheeler.com/flawfinder/)
can be used to find C API abuses, such as the use of strcpy and
statically sized buffers. Apple has documentation on implementing
“static analysis,” but it seems to have misunderstood this to mean
simply turning on compiler warnings (see http://developer.apple.com/TOOLS/xcode/staticanalysis.html).
A more promising application is the Clang Static Analyzer tool, available at http://clang.llvm.org/StaticAnalysis.html.
Like any static analysis tool, the clang analyzer has its share of
false positives, but it can be quite useful for pointing out
uninitialized values, memory leaks, and other flaws. To use this tool on
iPhone projects, you’ll need to do the following:
Open your project in Xcode. Go to Project | Edit Project Settings.
Go to Build. Change “Configuration” to “Debug.”
Change the Base SDK to “Simulator” of the appropriate OS version. “Valid Architectures” should change to “i386.”
Go to Configurations and change the default for command-line builds to be “Debug.”
In a terminal, cd to the project’s directory and run scan-build–view xcodebuild.
When the build completes, a browser window will be opened to a local web server to view results.
Consult
the clang documentation for more details on interpreting the results.
Of course, you should not assume that the use of a static analysis tool
will find all or even most of the security or reliability flaws in an
application; therefore, you should consider developing fuzzers for your
program’s various inputs.
The role of a fuzzer is to
expose faults through violating program assumptions. For instance, given
an application that parses an HTTP response and populates a buffer with
its contents, over-long data or format strings can cause the program to
fail in a potentially exploitable fashion. Similarly, integers
retrieved via external sources could fail to account for negative
numbers, thus leading to unexpected effects. You can craft fuzzers in
the form of fake servers, fake clients, or programs that generate many
test cases to be parsed by an application. For more information on
fuzzers, see http://en.wikipedia.org/wiki/Fuzz_testing.