• No results found

Chapter 5 Implementing a denormal tracing tool using Valgrind

5.3 Taint analysis

With the rise of the World Wide Web, web sites that delivered dynamic content via CGI scripts became common. Since these scripts were generally written by relatively inexperienced programmers, the inputs to the scripts were often used as arguments to potentially dangerous commands without first being sufficiently validated or quoted.

One example of this is ‘SQL injection’[BK04, Mon07]. If the user provides the username’ OR ’X’=’Xto the following script fragment, the query will return a list of all the users, instead of just the one required:

$name = $query->param("username");

$db->execute("SELECT * FROM users WHERE name=’$name’;");

This works because the WHERE clause becomesWHERE NAME=’’ OR ’X’=’X’which

is always true.

A similar problem called ‘shell injection’ occurs when scripts construct shell com- mands from insufficiently validated input strings and then execute them.[HYH+04]

$file = $query->param(’file’);

Here, if the user provides a filename such asfoo.zip; rm -rf /, the string passed to

the shell becomes/usr/bin/unzip -d /tmp foo.zip; rm -rf /, which is two com- mands — one to unzip a file, and the other to erase all files on the system.

Because of the prevalence of these kinds of programming errors, ataint modewas introduced into the PERL scripting language to help detect these problems.

When PERL’s taint mode is enabled, the PERL interpreter automatically associates a taint flag with every ‘scalar’2 value in a PERL script. Data internal to the script, e.g., literals and values derived entirely within the script are marked as untainted. However data that enters the script from external sources, such as environment variables, command line arguments, and the results of some system calls are marked as tainted. Operations on tainted data generate more tainted data; for example concatenating a tainted and an untainted string generates a new tainted string. Unlike MemCheck’s validity computations, PERL uses a pessimistic scheme to calculate the taintedness of the results of operations on data. It simply asserts that if any of the operands are tainted, then all the output is too. This may seem to have the scope to generate false positives, but since there is only a one bit taint flag per scalar, and the purpose of taint mode is to enforce string validation, it is a reasonable approximation for this problem domain.

If tainted data is used with a function that performs a potentially dangerous action, such as writing to a file, running an external program, or accessing a database, then the PERL interpreter generates an error and halts the script.

In PERL, the taint flag is removed from input data by first validating the data using a regular expression. The substrings returned from a regular expression match are flagged as untainted, and these untainted values can then be used safely with the actions mentioned above.

5.3.1 Taint analysis using Valgrind

By virtue of the fact that PERL is an interpreted language, and that all values in the language are strongly typed and have a data structure internal to the interpreter associated with them, it was relatively straightforward to add a taint mode to the PERL runtime. To perform dynamic taint analysis on arbitrary binaries is much more complex.

A 2005 paper by Newsome et al.[NS05] describes a Valgrind-based system called 2In PERL, scalars are the non-composite data values; that is to say individual strings, numbers, or references.

TaintCheck which provides tools to trace the propagation of user-generated data through a program and to detect if it is eventually used in dangerous ways. As with PERL’s taint mode, and Valgrind’s Memcheck, TaintCheck has three major components.

The first is called TaintSeed and marks data as tainted or trusted. Every byte

of memory and the CPU registers in TaintCheck is shadowed by a pointer to a taint data structure. When a program receives data from a network socket, TaintCheck’s default policy marks all the data received as tainted. The taint structure associated with each byte records the parameters passed to the last system call, the results from the call, and the user stack at the time of the system call. This later allows the developer to determine the initial source of the data.

TaintCheck’s second component is called TaintTracker and provides the taint

propagation of TaintCheck. The taint propagation policy is done on a per-byte basis, and is similar to that of PERL’s — loads and stores directly copy the taint information, and arithmetic and logic operations assume pessimistically that if any of the source operands are tainted, then the result should be too. TaintTracker includes a couple of special cases to handle some common x86 instruction patterns, such as XOR-ing a register with itself to zero a register (and thus making its value untainted). When propagating taint information, TaintTracker can either make the new value point at the same taint structure as the tainted source value, or create a new taint structure which records the current stack, and contains a pointer back to the earlier taint structure. This latter scheme consumes much more memory, but provides an exact chain of all the operations that propagated the tainted value from the point where it was initially injected into the program to the current moment in time.

The final component in TaintCheck is TaintAssert, which detects whether tainted

data is used dangerously. By default, TaintAssert defines as dangerous the use of tainted data as a jump address, or as the format argument to a printf-style function. These two scenarios cover a majority of internet-based security exploits. When dangerous uses are detected, they can be logged, along with a chain of all the taint structures for offline analysis.

As with PERL’s taint mode, TaintCheck focusses on detecting dangerous uses of user data that can potentially be used as attack vectors by malware. Because of this, the pessimistic propagation of taint information is appropriate and simplifies the implementation substantially.

5.3.2 Taint analysis and denormal tracing

TaintCheck tags user inputs as tainted, traces tainted data throughout the program, and warns and logs dangerous uses of tainted data. At first glance, it would seem that a taint tracing mechanism like this could be adapted for tracing denormal arithmetic. Certainly an analogue of TaintTracker could be written that instruments every floating point operation and if any operands or the output of an operation is denormal, uses shadow memory to ‘tag’ the value with the appropriate metadata. Any further uses of these tagged values could be examined to allow propagation of denormal values through the code to be monitored.

However, in practice there are two substantial difficulties with implementing a scheme like this. Firstly the differences in the ‘lifecycle’ of a denormal value com- pared to a tainted value, and secondly how a TaintAssert analogue might be defined.

5.3.3 Denormal vs taintedness lifecycle

In TaintCheck, there is a clear point where tainted data enters a program, and taint- edness is a well defined binary state: all the data read from a network socket is defined as tainted, everything else is not. Furthermore, at least for the common cases, since TaintCheck is using a ‘pessimistic’ approach, there is a reasonably nat- ural definition of how taintedness should be propagated as the result of various CPUinstructions that copy or operate on data previously marked as tainted. Be- cause of these definitions, although taintedness can be copied or propagated, it is impossible for tainted data to be createdde novo— taintedness always originates from a memory buffer written to by thereadandrecvfamily of system calls. Sim- ilarly, taintedness cannot be destroyed as such. Tainted values can be overwritten thus removing the value along with the taint, but taintedness on its own cannot be removed from a value.

The end point of the lifecycle of a tainted value is also well defined. Three possible things can happen to a tainted value: after some number of harmless operations are performed on it, it can be ignored for the rest of the program; or it can be overwritten by another value, thus destroying it; or it can be used dangerously, thus triggering TaintCheck to generate an error. In this third case, because the origin of all tainted data is explicitly recorded, and because each operation on tainted data may also be recorded, TaintCheck can build a complete history of everything that happened to that value, from its injection into the program, to its final dangerous use.

may be injected into a program as user data, but it is equally possible for a sequence of operations to create denormal values ‘mid program’ from what had been normal data. One common way this can happen is by some iterative process causing input values to converge on zero. Another is by a routine that uses the difference between two almost identical data sets.

By the same token, it is possible for the denormalcy to be removed from a value while preserving some of the information contained in it, either by adding random normal noise to it, or by scaling it to a sufficiently large value. Denormals can also be removed by flushing to zero, but this is the equivalent of overwriting the denormal with±0, thus destroying it.

These two facts combined mean that unlike for taintedness, short of recording a trace of every operation in a program, there is no clear way to identify the history of a given denormal once it is detected. Even if the original user data was denormal — something of which there is no guarantee — the data may have become normal at some point in the program, and then denormal again later on.

The problem of an incomplete history is compounded by the fact that because of the design of the x86 instruction set, and because of Intel’s calling conventions, often an optimising compiler will perform floating point copies, floating point assignments, and manipulate the call stack on function entry/exit using integer instructions.3 These optimisations mean that the first operations on some user data might be to copy them to the stack using integer instructions. Even if the floating point value is itself denormal, it must be determined that the memory area in question is definitely used for floating point values. If not, a tool will incorrectly flag integer instructions writing small integer values to memory4 as denormal. Values in this range commonly occur as flags and loop indices. This inability introduces further gaps into the history of an eventually denormal value.

5.3.4 Reporting denormal events

For TaintCheck, not only is there a clear and unbroken lifecycle for tainted data, there is also a small set of discrete operations that are regarded as dangerous and when they occur an error can be reported.

From the performance point of view, the very fact that denormal arithmetic occurs at all is a problem, so for a denormal tracer there is no clear distinction between the 3This issue was also a limitation with DIP inSec. 4.4. Some of these kinds of optimisations can be seen inAppendix D.

4All 32-bit integers less than 8,388,608 except 0 have the same binary representation as 32-bit floating point denormals.

operations that cause a denormal to be propagated and a dangerous operation that should be reported.

Also, from a performance point of view, individual denormal operations are not of much interest — what needs to be determined is the how many of them occur, how are they distributed throughout the program’s data sets, and whether they persist over time or not. A denormal tracer that simply records individual events will generate a huge trace log that may not offer much insight into a program’s behaviour. This implies that unlike for a taint analyser an important aspect of a denormal tracer is how the raw trace data is reduced and reported.