In my previous blog
post on this topic, titled “Application Security Part I: Whose Responsibility is
it?”, I explored the responsibility of security in the
mobile app ecosystem. In this post,
let’s take a little deeper look at the problem of security and code safety from
a mobile app developer’s point of view and explore what developers need to
think about and how they can avoid potential security problems in their
applications.
The majority of
security problems in app development are really software quality problems.
There’s something wrong with the way the code was written that leaves a door
open for someone to exploit.
Fortunately, many of these problems can be easily fixed. Some of these can be fixed in the design
phase of product development and others in the code phase. We’ll explore a few examples of each type in
this post.
The Objective of this Post
Now, before we jump
in, I need to confess that this is a broad and deep subject. My goal here is to present a high-level
understanding of code security through discussion and a few examples that will
help you understand the kinds of vulnerabilities that can lead to
problems. I will also provide references
to further reading for those that really want the “Full Monty” on developing
secure apps.
Code
Security at Design Time
·
Design with security in mind. This is perhaps the most important thing you
can do. Think about the following questions before you design your code
and have answers for all of them:
a) What
Assets does this software need to protect?
Credit Card numbers, user data, contact lists, account info, access to
paid services, and privileged access to device. To protect sensitive assets, you need to plan
how you’re going to deal with the data at design time. If you’re sending
sensitive data over a computer network, you should use SSL/TLS protocol to
prevent attackers from eavesdropping. Look into OpenSSL for open source
libraries and code examples on how to use this protocol. Also, for server-side storage, consider using
a 3rd party database such as MySQL. These systems have their own
built-in security policies that you can leverage. Also, for secure authentication, leverage
trusted services like OAuth and OpenID (See Additional Resources for more info
below).
b) How
might an attacker exploit my code? This
is often called “Thread Modeling”.
Attackers might be eavesdropping on the network ports you use, or
providing unexpected inputs that overflow memory buffers and inject attacker-supplied
code to be executed. You have to ensure, for example, that you never execute
code on behalf of an untrusted user.
2. Follow
a Secure Coding Standard - Select a secure coding standard and
make sure your code conforms. A secure
coding standard will identify specific issues with a particular programming
language that a compiler or analysis tool might fail to diagnose. Secure coding standards also define
requirements for producing code level security in your system. Several popular secure coding standards are available
from CERT® on their website at www.securecoding.cert.org.
3. Perform
Design Reviews. This
is a simple thing teams can do, and it can really pay off in the long run. A few years ago, I managed a couple of
engineering teams at Sun and when a new code module was planned. The engineering lead would present the design
to a team of Senior Architects with different backgrounds (JVM, networking, platform
architecture, etc.). This always uncovered
potential problems –and did so early on before they became big problems that
were expensive to fix.
4. Understand
Emerging Threats. Pay
attention to the type of exploits that are popular, and make sure you’re not
helping the bad guys. Designate one
person on your team to stay up to date on the current security trends and put
her/him in the code review. A great place to start is by visiting the CERT website frequently
and becoming familiar with all the resources they offer. In addition, all major platform and OS
providers publish security updates from time to time as issues are uncovered.
5. Use
Static Analysis Tools. Static
code analysis can really help locate many kinds of software validation and
reliability problems, including many memory problems. Some examples of
companies that provide these products include:
Klocworkand Coverity, Parasoft,
and Vericode. A
more complete list can be found here: http://www.cert.org/secure-coding/tools.html
If you’re doing all these things, you’re actually doing
pretty well. You’ve got a solid design, you’re
following a secure coding standard, you’ve got someone keeping an eye out for
potential security exploits, and you’re conducting design reviews and running
static analysis tools on a regular basis. You’ve got the design side covered
pretty well. Now let’s look at some
specific code issues that can cause problems.
Code
Security at Development Time
Most of the following examples demonstrate some of common
problems found in native C and C++ development.
Web developers and Java developers can still benefit as the concepts
behind the problems are valid in many languages. In the Additional Resources section below, I
provide links to specific Web and Java resources for code security.
1)
Application Frameworks
Use application frameworks
whenever possible. Frameworks hide a lot
of the nasty memory management and secure network connection issues from the
developer and reduce the possibility of making simple, but costly, mistakes. Fortunately, the BlackBerry platform supports
a lot of great developer frameworks, both from BlackBerry (such as the Invocation
and Share Framework, and the UI Framework Cascades)
and through BlackBerry’s
Platform Partners.
2) Memory Management
Aside from the obvious allocation
of memory insufficient for the data you’re writing into it, there’s a common
issue people new to C and C++ sometimes experience regarding memory
management. The problem occurs when you
confuse which action you use to release the memory you’ve allocated.
Using Cascades will reduce
the likelihood of memory management problems because it hides all the messy
details for you within the framework.
However, if you must allocate memory yourself for your app, keep the
following tip in mind.
There are two main C
functions for allocating memory: malloc() and
calloc(). Use malloc()
when you don’t care about initializing the data in memory. Use calloc() if
you want to initialize the memory to 0.
The important thing to remember is when using either malloc() or calloc(),
you must use the function free() to release the memory and
give it back to the system.
In contrast, when using the
C++ method new() to create a new object, you must use the
corresponding delete() method.
Nothing good can happen when you call free() on memory you’ve created
using new().
3) Function Safety
The QNX platform provides a number
of preferred C functions that are safer to use than the more commonly known
standard functions. A subset of these is
shown in the table below. You can find the
complete list in the BlackBerry Native SDK online docs (referenced in
Additional Resources section below).
As with memory management,
using an application framework helps with function safety as well. For instance, the Cascades classes such as
QString and QByteArray protect you from many of these problems as well.
As you’ll see in these
examples, most of the serious problems occur when buffers you’re reading into
or writing out to are not large enough to take the data. These functions below help you from
over-writing some of the time. You
should always perform bounds checking if you want to be on the safe side.
Unsafe Function(s)
|
Preferred Function(s)
|
Comments
|
strcpy() and strncpy()
strcat() and strncat()
|
strlcpy() and strlcat()
|
The
function strlcpy()
copies strings and the function strlcat() concatenates strings. They're
designed to be safer, more consistent, and less error-prone replacements for strncpy()
and strncat(). Unlike those functions, strlcpy() and strlcat() take the full size of the buffer (not just the length) and guarantee to NUL-terminate the result (as long as the size is larger than 0 or, in the case of strlcat(), as long as there's at least one byte free in the destination string).
There also exist "wide" versions of these functions
that are equally dangerous: wcscpy(), but there is no "l"
safe version to use, only wcsncpy()
which does not necessarily NUL-terminate the output. Care must be taken to
ensure the output buffer is NUL-terminated.
|
sprintf() and vsprintf()
|
snprintf() and vsnprintf()
|
The
snprintf()
function is similar to fprintf(),
except that snprintf()
places the generated output (up to the specified maximum number of
characters) into the character array pointed to by buf,
instead of writing it to a file. The snprintf() function is
similar to sprintf(),
but with boundary checking. A null character is placed at the end of the
generated character string. |
gets()
|
fgets(buf, n, fp)
|
The
fgets()
function reads a string of characters from the stream specified by fp, and stores them in the array specified by buf, limited to size n. |
getwd()
|
getcwd(buffer, size)
|
The
getcwd()
function returns the name of the current working directory. buffer is a pointer to a buffer of at least size bytes where the NUL-terminated name of the current
working directory will be placed. The maximum size that might be required for
buffer is PATH_MAX + 1 bytes |
4) Structures
Structures
in C and C++ are aggregated types that define and contain other data elements
within them. The elements of a structure
cannot be re-ordered by the compiler.
Modern compilers do use a variety of methods to minimize the security
risk of stack-buffer overflows such as stack canaries, address-space layout
randomization, re-ordering the local variables within a function, among other
things. However, since the compiler
can’t re-order the elements within your structures, the possibility of a buffer
overflow on one of your elements affecting function parameters or local
variables remains. Consider the
following example:
struct _JOB {
char name[64];
char title[64];
DATE startdate;
DATE enddate;
WAGE salary;
} JOB, *PJOB;
The buffers name and title can both overrun. Since they’re placed on the stack first, the
elements defined after them in the structure and on the stack itself can be
affected. For this reason, care should
be given when defining the elements in a structure. The next example shows a structure that's
defensively designed:
struct _JOB {
DATE startdate;
DATE enddate;
WAGE salary;
char name[64]; // Buffers placed at end
char title[64]; // of struct definition
} JOB, *PJOB;
Recommendations
for using structures
When dealing with structures that contain
fixed-width buffers or arrays designed to receive data that's controlled or
influenced by a user:
·
Buffers and arrays in structures should be
grouped at the end of the structure
·
Local variables declared as structures
should be declared after local buffers but before any other local variables
·
Global variables declared as structures
should be declared before any global buffers and arrays and after any other
global variables
·
Pointers to structures do not need any
special consideration
·
Where practical, try to minimize the number
of local variables cast as structures with buffers and arrays as elements
5) Macros
Macros are one of my
favorite mechanisms in C and C++. I love
using them; however, you have to be very careful as they can get you into
trouble. Macros are defined through the
use of the #define preprocessor directive and when processed, literally expand
in your source code prior to compilation.
If IDE’s could show you what the processed source code looked like
(maybe some do?), then I suspect we’d see fewer problems. Consider the following example that
demonstrates the issue:
#define CUBE (x*x*x)
…
int x = CUBE (5-2)
In this example, you might
expect that you’re going to get the cube of 3 which is 27. However, when the preprocessor expands the
macro, normal operator precedence rules apply.
So, here’s how that the value of x will be calculated:
CUBE(5-2) = (5 – 2*5 – 2*5 – 2)
CUBE(5-2) = (5 – 10 -10 -2)
CUBE(5-2) = -17
Therefore, to protect against this problem, the macro can easily be defined using parentheses as in:
#define CUBE(x) (x)*(x)*(x)
In this example, we’ve seen how
operator precedence rules can get you into trouble with macros. There are other problems such as the
importance of white space when defining macros, using macros in if statements,
and self-referencing macros to name a few.
Provided you think about how the macro will expand in your source and
you consider how the arguments you pass to macros will be interpreted in that
expansion, you should be fine.
6) Integers (signed, unsigned) and
Enumerations
Another common problem that
can expose serious threats to your code involves integer overflow or
underflow. This can happen when care is
not taken with integers. The following
code fragment demonstrates how serious this problem can be (recall our
discussion about buffer sizes above):
int buffLen = 0;
printf(“[buf] %d %u\n”, buffLen, buffLen);
buffLen = -1;
printf(“[buf] %d %u\n”, bufflen, bufflen);
Executing this code gives:
[buf] 0 0
[buf] -1 4294967295
In this example, I forced
the value of bufLen to be -1 for demonstration purposes. But, it’s easy to imagine a simple arithmetic
error in a length calculation to be off by one.
Similar problems arise with
the use of Enumerations. Not all
compilers use the same kind of “int” for Enumerated types. So, you need to be careful when using
Enumerations. Make sure you know how
your compiler treats them. If your
compiler uses signed integers by default (such as Microsoft Visual C++ and ARMCC), it’s not possible to
create an unsigned enumeration as the value will be overflow the signed int
(for example, you can’t set the enumerated value to be 0xffffffff).
Summary
Though we’ve just
scratched the surface of this topic, we’ve discussed a number of things
developers can do to protect their code from security problems. We’ve explored good practices developers can
adopt at design time, such as:
·
Design with security in mind
·
Follow a Secure Coding Standard
·
Perform Design Reviews
·
Understand Emerging Threats
·
Use Static Analysis Tools.
We’ve also looked
at some common coding mistakes that can lead to security problems and discussed
how the code can be fixed to avoid these problems. We’ve seen how leveraging Application
Frameworks (like Cascades) can greatly reduce security problems. As an app developer, you have a
responsibility to protect your user’s identity and sensitive data as best you
can from attackers seeking to exploit it.
The content in this blog post should help you get started with some good
practices and links to learn more. For more information on application security, additional examples, and deeper analysis, please refer to the additional resources below.
Additional Resources
·
Seacord,
Robert. The CERT C Secure Coding Standard, Addison Wesley, 2008
·
Seacord,
Robert. Secure Coding in C and C++, 2nd Edition, Addison
Wesley, 2013.
Acknowledgements
I’d like to thank Robert Seacord for taking the time to
read an early draft of my blog and provide helpful feedback. Robert is a senior analyst in the CERT®
Program at the Software Engineering Institute (SEI) in Pittsburgh, PA where he
leads the Secure Coding Initiative and is author of a number of books on Secure
Programming. I’d also like to thank the
BlackBerry Security team for their insightful comments and suggestions.
No comments:
Post a Comment