Introduction
n.bug is a library call trace implementation for Windows NT operating systems. It is meant as a tool to quickly analyse specific
library calls a binary program makes during runtime.
A typical n.bug trace run consists of the following
steps:
1. User selects the functions to trace
2. User selects either a running process or a program
path (called the target)
3. User initiates trace
4. n.bug attaches a debugger engine to the target
and executes it or resumes execution
5. (optional) User interacts with target
6. User terminates trace
7. User inspects trace results and optionally
saves them to a file
8. (optional) User repeats the process with different
settings
As the result of a trace session, n.bug outputs
the following information for each library
function call detected and matching the trace
definition:
• The caller’s return address (points
to the instruction after the call
instruction)
• The module and function name called
• The type, name and value of the parameter
upon entry into the library function
• The type, name and value of the parameter
upon exit from the library function
• The return value of the call
The results of a trace session
can be saved into a text file for future reference and
documentation purposes.
General concept of Runtime Analysis
Runtime analysis inspects the code of a target process
during its execution. In contrast to
static analysis, which takes the processes code and data
information and tries to determine the
possible execution paths, runtime analysis does not suffer
from the many cases in which static
analysis cannot determine variable parts of the code
flow.
On the other hand, runtime analysis is
always limited to the code executed, since only
this is
what is inspected. Consider the following code example:
void vulnerable_func(char *str) {
char buf[255];
if ( check_formatting(str) ) {
do_something( str );
} else {
sprintf(buf,"Error in string: %s\n",str);
log( buf );
}
}
When doing runtime analysis, normally the
correct interaction is used with the target process. In
the case of the function above, the vulnerable part is
located in the handling of exceptional circumstances, where
check_formatting(str) is failing.
Runtime analysis tries to regain a level
of abstraction from the binary code executed.
Manual inspection of all binary code in a target is
unfeasible. Therefore, levels of abstraction
are required to speed up the process of bug finding.
Runtime analysis tries to inspect parts of the
code flow and give an output, which abstracts
the functionality covered by the binary code.

(größere Ansicht)
Library call tracing
Most high-level languages use several levels
of abstraction to hide small details of data
processing and operating system interfacing from
the application developer. The order of
abstraction is roughly as follows:
1. OS Kernel Functionality, exposed by Software
Interrupts
2. Basic operating system functionality
a. Exposed by low level libraries (eg. libc)
b. Exposed by High-level OS API
3. Application framework functionality, exposed
by derivable classes
Simple runtime analysis will trap
and record all calls to the OS Kernel
(1) and report them to
the user. This approach is used in many UNIX
environments with tools such as “strace” or “truss”.
On a Windows 32 Platform, this approach
is not feasible, since the Kernel API
changes with releases and is intentionally
not documented.
Library call tracing traps the calls to both
low-level libraries as well as high-level
OS API
libraries and records their arguments. This
gives the user a more abstract view on the
operation of a specific piece of code. n.bug
in particular will set breakpoints on all library
calls it is supposed to trace and report the
arguments of the calls, both upon entering and
exiting the library function.
It should be permanently in the mind
of the user that library call tracing is not complete.
It only covers the code executed and
it only covers functionality in the binary
implemented by known and traced library
functions. Custom code using pointer
arithmetic or code using custom libraries
with functions not defined to trace are
not covered and will not show up in the
report.
The user
should also keep in mind that only functionality
executed after the start of a trace is covered. If n.bug
is attached to a process (e.g. a server) and no interaction
with the server takes place, only the calls
in the server’s
idle loop are traced, which are
probably not of any
interest. To achieve a maximal effect,
the user should try to interact with
the target process in
as many different ways as possible.
For example, a user authentication should be tried with
different usernames and passwords,
both correct and wrong, using as many account types as
possible.
Trace definition files
n.bug uses trace
definition files to know the functions
to trace and their type and number of
arguments. The trace files in general follow a C compatible
syntax for function prototypes.
A simple trace definition looks
like this:
int sprintf([out] char * buf, [in] fmchar * format );
int swprintf([out] wchar *buffer, [in] fmtwchar *format );
int vsprintf([out] char *buffer, [in] fmtchar *format, [in] void * argptr);
A prototype definition must at least contain a
return type, a function name, the parentheses
and a terminating semicolon. A more complete
specification of the same function to trace would
be:
int msvcrt:system([in] char *commandline);
This includes the exact module
where n.bug should look for the function, hereby
disabling all other implementations of system
in other modules. Do not specify a general trace
definition and a module specific together in the
same trace file (see Bugs and glitches). It also
gives the argument a name, which is then displayed
in the result window and the saved report. The
[in] specification tells n.bug that the parameter
is of interest when entering the function only.