AD1114767
AD1114767
POSTGRADUATE
SCHOOL
MONTEREY, CALIFORNIA
THESIS
by
Eric C. Wikman
June 2020
i
THIS PAGE INTENTIONALLY LEFT BLANK
ii
Approved for public release. Distribution is unlimited.
Eric C. Wikman
Captain, United States Marine Corps
BS, Auburn University, 2011
from the
Cynthia E. Irvine
Co-Advisor
Peter J. Denning
Chair, Department of Computer Science
iii
THIS PAGE INTENTIONALLY LEFT BLANK
iv
ABSTRACT
v
THIS PAGE INTENTIONALLY LEFT BLANK
vi
Table of Contents
1 Introduction 1
1.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Research Plan. . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3 Thesis Organization . . . . . . . . . . . . . . . . . . . . . . . 2
1.4 Key Terms . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2 Background 5
2.1 Buffer Overflow Vulnerabilities. . . . . . . . . . . . . . . . . . . 5
2.2 Existing Methods that can Detect Overflows. . . . . . . . . . . . . . 6
2.3 Ghidra . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.4 NIST Juliet Test Suite . . . . . . . . . . . . . . . . . . . . . . 9
3 Design Approach 11
3.1 Primary Causes of Buffer Overflows . . . . . . . . . . . . . . . . . 11
3.2 Detecting Overflows at Vulnerable Sinks . . . . . . . . . . . . . . . 12
3.3 Method for Finding Overflows . . . . . . . . . . . . . . . . . . . 13
4 Design 17
4.1 Functional Requirements . . . . . . . . . . . . . . . . . . . . . 17
4.2 Design Choices . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.3 Constraints . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4.4 Limitations. . . . . . . . . . . . . . . . . . . . . . . . . . . 21
4.5 Functional Behavior . . . . . . . . . . . . . . . . . . . . . . . 22
4.6 Core Modules . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
5 Implementation 31
5.1 Global Variables . . . . . . . . . . . . . . . . . . . . . . . . 33
5.2 Sink Module . . . . . . . . . . . . . . . . . . . . . . . . . . 33
vii
5.3 Source Module . . . . . . . . . . . . . . . . . . . . . . . . . 40
5.4 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
6 Test Plan 67
6.1 Behavioral Test . . . . . . . . . . . . . . . . . . . . . . . . . 67
6.2 Functional Tests . . . . . . . . . . . . . . . . . . . . . . . . . 67
6.3 Expected Outputs . . . . . . . . . . . . . . . . . . . . . . . . 70
6.4 Calculating Error Rates . . . . . . . . . . . . . . . . . . . . . . 75
7 Testing Results 77
7.1 Behavioral Tests. . . . . . . . . . . . . . . . . . . . . . . . . 77
7.2 Functional Tests . . . . . . . . . . . . . . . . . . . . . . . . . 78
7.3 Discussion . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
8 Conclusion 87
8.1 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
8.2 Future Work . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Appendix C Compilation 99
viii
Appendix G Functional Test Cases 139
ix
THIS PAGE INTENTIONALLY LEFT BLANK
x
List of Figures
Figure 4.1 Class diagram for the buffer overflow detection program . . . . . 24
Figure 5.2 Source tree with the sink as the root (repeat from chapter 3) . . . 44
Figure 5.3 The use of offsets from function parameters. Left, disassembly of
the strcpy(). Right, the C source code. . . . . . . . . . . . . . . . 48
Figure 5.5 Source tree highlighting how the sources are filled . . . . . . . . 61
Figure 7.2 Incorrect source identification due to pointer use. The left is
the original disassembly. The right is the marked-up version. . . . 82
Figure 7.3 Disassembly of the stack layer for the source test . . . . . . . . . 84
Figure A.3 Ghidra script manager for view, editing, and running scripts . . . 93
xi
Figure E.1 Test 1: Function with no parameters . . . . . . . . . . . . . . . . 120
Figure E.4 Test 4: Function with two parameters passed as pointers . . . . . 123
Figure E.6 Test 6: Function with one parameter and a function call . . . . . 125
Figure E.7 Test 7: Function with two parameters and a function call . . . . . 126
Figure E.8 Test 8: Function with seven parameters and a function call . . . . 127
Figure E.10 Test 10: Function with uninitialized struct pointer . . . . . . . . 129
Figure E.11 Test 11: Function with struct passed by value . . . . . . . . . . . 130
Figure E.12 Test 11: Function that calls with a struct passed by value . . . . . 131
Figure F.1 CSV file containing the sources that can overflow a source . . . . 138
xii
List of Tables
Table 6.2 The test cases came from the Juliet test suite in the CWE 121 Stack
Based Buffer Overflow category [3] . . . . . . . . . . . . . . . . 70
Table 6.3 Results of the manual inspection of the source code and disassembly 75
Table 7.1 Summary of the results from the buffer overflow detection method 78
Table 7.1 Summary of the results from the buffer overflow detection method 79
Table B.1 Flow Variants for the Juliet test suite [3] . . . . . . . . . . . . . . 98
xiii
Table G.7 Summary of Test Case 7 . . . . . . . . . . . . . . . . . . . . . . 147
xiv
Table G.31 Summary of Test Case 31 . . . . . . . . . . . . . . . . . . . . . . 171
xv
Table H.15 Summary of the results from test case 24 . . . . . . . . . . . . . . 202
xvi
Listings
Code E.1 Source Code for testing Ghidra’s decomiler on assembly code · · · · 115
xvii
THIS PAGE INTENTIONALLY LEFT BLANK
xviii
List of Acronyms and Abbreviations
xix
THIS PAGE INTENTIONALLY LEFT BLANK
xx
Acknowledgments
First, I would like to thank my advisors Professor Irvine and Professor Nguyen. This was
a challenging time, both in the world and in our personal lives. Despite everything, they
were always there to lend their knowledge and guidance. I truly appreciate the time and
energy they gave to the success of this thesis. Next, I would like to thank my cohort for the
camaraderie we built while at NPS. It was always good to know that no matter what the
challenge we could always look towards each other for help. Last, I would like to thank my
wife and son for being my inspiration and source of joy. Especially my wife, for humoring
me when I talked about buffer overflows. Thank you for making this NPS journey an
amazing experience.
xxi
THIS PAGE INTENTIONALLY LEFT BLANK
xxii
CHAPTER 1:
Introduction
Buffer overflows are common vulnerabilities in software where an executing program writes
to a location outside of the intended boundary of a memory resource (e.g., an array of
characters in the stack segment). In most cases, this causes the program to crash. In
more dangerous situations, an attacker can exploit a buffer overflow to allow remote code
execution. Whether the objective is fixing bugs or creating a secure program, being able to
discover buffer overflow vulnerabilities can be exceedingly valuable for developing quality
software.
Buffer overflow vulnerabilities can be exploited using a variety of techniques. One method,
which is the focus of this thesis, is the occurrence of overflows caused by passing bad
parameters to vulnerable sinks. In this context, a sink is a function that receives inputs
as execution parameters. Functions that perform little to no bounds checking on their
parameters have a potential vulnerability for a buffer overflow. Security researchers often
wish to identify the parameters that are supplied to vulnerable sinks. The goal of discovering
the parameter values is to determine which combination of parameters could result in an
overflow. This process is exceedingly tedious because large programs may contain many
vulnerable sinks with many possible sources of parameters. This thesis aims to automate
the process of detecting potential stack-based buffer overflow vulnerabilities in binary code
using static analysis techniques.
1.1 Motivation
Being able to detect buffer overflows would improve software assurance for the DoD.
Static analysis can be used as a baseline for determining the susceptibility of a program to
cause buffer overflows. This gives the DoD an idea of the overall quality of the software
based on its vulnerability to buffer overflow attacks. The research also benefits the reverse
engineering field and automates the discovery of buffer overflows. This could save time
and energy for reverse engineers attempting to discover vulnerabilities in a program.
1
1.2 Research Plan
A stack buffer overflow condition occurs when a variable on the stack is accessed beyond its
declared value. This occurs commonly in programming languages such as C or C++, where
variables are mapped to memory. Most of the time, these errors are caused by improper
input validations within the program. Good input validation checks the type and size of an
input before using the input. When these checks do not occur, a program may fail when
it tries to access memory locations that are not allocated to the variable. This can occur
in functions such as strcpy(), where the source string is copied into the destination string.
Strcpy() does not check the bounds of the input strings and instead uses the null terminator
in the source string to know when to stop copying. If the source string is bigger than the
destination string, strcpy() will write past the end of the destination string, causing a buffer
overflow.
This research investigates the viability of a buffer overflow detection method to statically
analyze binary files for buffer overflows. The research focuses on the discovery of stack-
based buffer overflows that occur in C programs compiled on a Linux x86 64-bit system.
Using Ghidra [1], a reverse engineering tool developed by the National Security Agency
(NSA) as a foundation, a Python script was developed that uses the Ghidra API [2] to analyze
programs for buffer overflow vulnerabilities. To evaluate the method’s effectiveness, the
National Institute of Standards and Technology (NIST) Juliet test suite [3] is used to
determine the error rate of the method.
2
1.4 Key Terms
The following key terms are used throughout the thesis.
3
THIS PAGE INTENTIONALLY LEFT BLANK
4
CHAPTER 2:
Background
This chapter reviews how buffer overflows are caused as well as related research on the
discovery of buffer overflows. This chapter also reviews the reverse engineering tool
Ghidra [1] and discusses how it can be used to discover buffer overflow vulnerabilities. The
last topic covered is the use of the NIST Juliet Test Suite [3] and how it can be used to
determine the effectiveness of the buffer overflow detection.
5
Table 2.1. CWE Top 10 most dangerous software errors of 2019
The MITRE Corporation compiled a list of the most dangerous software errors
according to CWEs based on NIST CVE data and CVSS scores. The highest
scoring CWE is the result of a buffer overflow. Adapted from [6, Table 1].
buffer overflow vulnerability did not have the highest severity among the top errors, but it
was the most frequent vulnerability among the other errors. This indicates that coding a
buffer overflow vulnerability is common, and it is hard for developers to catch these types
of errors in testing. Having a means to detect this vulnerability during development would
help reduce the number of CVEs reported for this type of error.
6
2.2.1 ARCHER (ARray CHeckER)
ARCHER is a constraint solver tool that checks the bounds of the variables and memory
sizes of various objects. ARCHER statically analyzes the source code for memory constraint
violations such as array accesses, pointer dereferences, or calls to a function that expects a
size parameter [10].
7
of executables. This buffer overflow detection method focused on the detection of buffer
overflows caused by loops [15].
2.3 Ghidra
Ghidra is a JAVA-based software reverse engineering (SRE) framework that was developed
by the NSA. Ghidra was released to the public in March 2019 and the source code was
released the next month on April 4, 2019. Ghidra is the result of about 20 years of
development [21]. NSA defines Ghidra as a, “SRE framework developed by NSA’s Research
Directorate for NSA’s cybersecurity mission. It helps analyze malicious code and malware
like viruses and can give cybersecurity professionals a better understanding of potential
vulnerabilities in their networks and systems” [1].
Approximating the local variables plays a large role in identifying the allocated space on
the stack. Without Ghidra, a user would have to look at a disassembled function and
8
determine how much space is allocated to local variables. This is typically done by looking
at the beginning of the function for the SUB RSP, 0x** instruction. However, there are
no indications where one local variable starts and another one ends. Instead, the user
would have to go through the tedious process of mapping all of a function’s stack offsets.
Fortunately, Ghidra can perform this analysis to allow users to search all of the found stack
references versus searching the function. Once a specific stack reference is known, simple
arithmetic can be used to determine the space that is allotted for that reference.
In Ghidra version 9.1-BETA, there are no features that perform the buffer overflow detection
task. However, using the Ghidra API, scripts can be implemented to perform various
functions such as finding, commenting, and changing functional aspects of the binary file.
The research presented here implements a Ghidra script to search a binary file to discover
buffer overflows. The script also makes assumptions about the binary file that Ghidra’s
analysis does not accurately depict. The detailed short falls of Ghidra are discussed in
Chapter 5, which also includes a discussion of how they can be overcome.
9
THIS PAGE INTENTIONALLY LEFT BLANK
10
CHAPTER 3:
Design Approach
This chapter discusses how static analysis of a binary file can detect buffer overflows in
programs. This research investigates cases where buffer overflows occur in libc functions.
The libc functions capable of producing a buffer overflow are referred to as vulnerable sinks.
In this context, a sink is a function that receives inputs to execute its code. The chapter
covers how sinks can be found in a program and overviews a process for detecting buffer
overflows from the sinks based on their sources. Sources are values or variables that have
space or information allocated to them at a particular place inside the program [4]. In cases
of values, the source may be stored in a register before a function call. In cases of variables,
the source may be located on the stack or heap. This work models a technique used by
security analysts to manually discover buffer overflows from vulnerable sinks (i.e., tracing
the sink’s parameters back to their sources). This method, named Overflow Detection
from Sinks and Sources (ODSS), allows the analyst to determine if there is a mismatch in
parameter sizes that would cause an overflow. The method discussed here is a means of
automating the manual process for discovering buffer overflows.
11
string and the function returns [25]. If the source string is larger than the destination
buffer, then the function continues to write past the end of the destination buffer, causing
an overflow. Strcpy() has no internal mechanism for checking if the destination buffer is
greater than or equal to the source string. Thus, it is the responsibility of the programmer
to perform those checks prior to calling strcpy(). Vulnerable sinks, such as strcpy(), are a
common entry points for attackers to cause buffer overflows. For this reason, performing
analysis on these vulnerable sinks would allow the discovery of potential overflow vectors.
When looking at source code, finding the parameters’ sources is an easier task. The
parameter name would resolve to some initialized variable or input parameter for a given
function. Initialized variables are clearly stated with the size of the array or the string that
is used to initialize the variable. Changes to a variable can be determined by searching
for the variable’s name, then determining how the variable is used in the instruction. It
becomes more complex when sources are passed as parameters, but the actual location of
each source can still be determined. To do so would require getting all the references to
the called function and checking the variable or value that was used by the calling function.
Finding the sinks is similar, because a sink’s name can be searched in the program.
When looking at the disassembly of a program, most of the sources and sinks become
addresses. When the sources are local variables, they become offsets in the stack. Based
on one stack offset alone, it is difficult to determine how much space is allocated to a
12
particular local variable. To determine the variable’s allocated size, the start address of the
previous local variable needs to be determined. This allows the difference between the two
stack locations to be calculated, which results in the allocated size of the source variable.
Parameter passing becomes even more difficult because in 64-bit x86 systems the first six
parameters are passed in registers.
This research investigates detecting overflows based on sources and sinks. Unlike Archer,
Boon, and Uno, our method looks at disassembly information as opposed to the source code.
This also differs from value set analysis, because the focus is on sinks and not loops. Our
method consists of four main processing steps: find the sinks, find the sources, determine
the source usage, and calculate the overflow. Every CALL instruction to a vulnerable sink
has the potential to cause a buffer overflow, so naturally CALL instructions to the sink
are the first place to check. At each of the sink calls, the sources are passed to the sink
to perform the sink’s operation. These source values can come from any location in the
program. This means that their actual location on the stack needs to be determined for
each parameter passed to the sink. Once all the sources have been collected, they can be
analyzed to determine their allocated size and fill amount. Once all the sources have been
determined for a given sink, the sources can be compared to determine which combination
of sources could cause an overflow.
13
database, which contains reference information about functions, global variables, initialized
data section(s), and the uninitialized data (bss) section. The reference database keeps track
of the addresses from which each function is called. The references to the sinks contain the
parameters passed to the sink. This means the references to the sinks becomes the starting
point to search for the sources.
Finding sources starts with the call address to the sink. Since sinks in this research have
less than six parameters, the parameters are stored in registers. By tracking the register
usage backwards through the code, it is possible to determine values that are loaded into the
registers. These values can vary from integer values, local variables, function parameters,
or addresses. For integer values, local variables, and addresses, the search ends because
these values indicate a location or size, and thus, the source has been found. When source
values originate from a parameter or pointer, then the source’s true location still needs to be
determined. In these cases, all the references to the function or pointer need to be checked.
In the case of a function parameter, a path tree is formed where the call to the sink is the
root and the functions where the sources are located are the leaves and interior nodes. For
the tree to be built properly, repeat functions cannot appear on the same path. This means
that the method does not double search functions to prevent loops in the tree. Instead, the
method looks at every function once along a particular tree path. Figure 3.1 shows a sink
that can have multiple sources used as its parameters. The called sink takes two parameters
and for simplicity all the parameters have the same name between functions. Both sink’s
parameters come from the parameters of Func1. Both Func2 and Func3 call Func1, so they
are added to the tree. Func2 is similar to Func1 because both of the parameters in the call
come from Func2’s parameters. In Func3, par2 is allocated inside the function. For Func4,
Func5, and Func6, the parameters relevant to the sink are found inside their respective
functions.
14
Figure 3.1. Source tree with the sink as the root
15
3.3.4 Calculating the overflow
To calculate overflows all sources for a given sink are compared to determine if a buffer
overflow is possible. When the allocated size of a destination parameter is smaller than the
allocated size of a source parameter, a caution message is produced. A caution message
does not mean that there is an overflow; it only indicates that it is bad practice to copy from
a source parameter that is larger than the destination parameter. When the maximum fill
amount of a source parameter is larger than the allocated size of a destination parameter, a
warning message is produced. The warning message indicates that an overflow is possible
based on the static analysis of the program.
16
CHAPTER 4:
Design
This chapter discusses the functional requirements, the design of the ODSS script, and
the high-level approach. The design section describes the major system modules and their
interconnections. The program is divided into two modules. The first module finds all sinks
used within the binary file and calculates the overflow for each sink. The second module
finds the sources for each sink and determines how the sources are used.
17
Ghidra’s reference finding API allows for a concise list of how and where various elements
are used in the binary file. Last, Ghidra’s iterating API helps determine where instructions
begin and end. This makes moving up or down through the binary file significantly easier.
This research utilizes Linux-based Ghidra version 9.1-BETA and associated API.
18
methods. Sizes of the variables that are allocated on the stack are known before run time.
When variables are allocated on the heap, their sizes can grow or shrink as the program
executes. This makes it difficult for static analysis methods to analyze heap-based variables
because the program flow would need to be determined to correctly calculate the space
allocated to the variable.
4.3 Constraints
This section describes the capabilities that are not in the scope of this thesis and are not
implemented
19
difficult to quickly find alloca() when searching through the program. This becomes a task
of finding the behavior of the alloca() function in the code versus determining if a buffer
overflow occurred. In short, functions similar to alloca() are not included in the buffer
overflow tests.
20
cause an overflow. A single-use overflow would be to copy 100 bytes into a 50-byte buffer
once. A continuous overflow would be to copy 1 byte into a 50-byte buffer 100 times.
4.4 Limitations
Identifying the limitations allows for an understanding of what the overflow detection
method cannot do. There are situations that cannot be solved because the problem is
considered undecidable. These limitations are not so much to restrict the implementation
of this detection method, but to instead identify what was not able to be solved.
21
4.4.4 Delineating the different data types inside structures
When variables of the C data type struct are allocated on the stack, the compiler ensures
that there is enough room to hold the data structures. In cases where a struct is declared but
uninitialized, the compiler would create a section of the stack that is reserved for that struct.
The starting locations of each of the data types inside the struct are unknown, until they are
referenced. The Ghidra experiments described in Appendix E show that the location of an
uninitialized struct’s data types may not be able to be determined if there are not enough
clues in the code.
The script produces two types of outputs. The first type is a console print out displaying
the warning or caution messages and the second type is in the form of a CSV file. There are
two CSV output files. Even if there are no buffer overflows, the two CSV files are always
created. The first CSV file contains specific information for each of the sources, such as its
name, location, size, and the sink with which they were used. The second CSV file contains
all the sources and sinks that can produce a warning or caution message. Sample outputs
from the script can be seen in Appendix F.
In the event that a source cannot be found, that source is dropped from the list of sources.
The script does not notify the user when a source is dropped. If the detection script identifies
the wrong source, the script may report either a false positive or a false negative.
22
the modules interact. The program consists of three modules: Main, Sink, and Source.
The Main module is responsible for the program’s execution flow. The Sink module is
responsible for finding, creating, and calculating the overflows for the sinks. The Source
module is responsible for finding, creating, and filling in the information for sources. Figure
4.1 depicts the class diagram for the buffer overflow detection program.
The Sink module consists of two classes: Sink_Handler and the Sinks. The Source module
consists of three classes: Source_Handler, Source_Finder, and Sources. The details of
each class’s variables and methods are discussed in Chapter 5. Methods that have double
underscores are private methods and can only be called from within the class. Methods
without the double underscore can be called by other classes that have an instance of the
class object. Section 4.6.3 describes how these modules interact inside the program.
Find Sinks
Without Ghidra, finding sinks would have been a tedious process of identifying all the calls
and mapping the call’s location in libc. This would require knowing the version of libc and
having maps to each function for each different version of libc. Fortunately, Ghidra takes
care of the mapping process and can identify which libc function is called. This allows
the module to search through Ghidra’s function database and compare the function names
with vulnerable sink names. Once a sink is found, a sink object is created containing the
information for the sink.
Calculate Overflow
The sinks have a common parameter format using a source string, a destination string, and
sometimes an integer value. The source string is where the information is coming from.
The destination string is where the values are written. Using the information that was
23
Figure 4.1. Class diagram for the buffer overflow detection program
collected on the sources, a simple range check can determine if the destination string can
be overflowed. In the cases of a three-parameter sink, the values are also checked against
the integer amount parameter to determine an overflow.
There are three cases that can result from calculating the overflow. The first is no overflow.
In this case nothing is reported. The second case is a caution, where the allocated size of the
source is larger than the allocated size of the destination, while the maximum fill amount
of the source is less than the allocated size of the destination. This condition produces a
caution message showing the information of the sources in the sink. The third case is a
24
warning, which occurs when the source’s string length is greater than the allocated size of
the destination. This case produces a warning message with the sources that are used at the
sink.
Find Sources
This method takes a sink as input. For each sink, the method determines all the calls
that directly reference the sink. These references are known as sink references. The sink
references are the first locations checked for finding sources for a particular sink. The
Get_Sources method controls the order in which functions are checked for the discovery of
sources. The Get_Sources method builds the path with the sink references of a particular
sink as the root and subsequent function calls as the leaves and interior nodes.
The next step is to determine how the sources are passed to the sink. The sink references
have an address where the sink is called, which is referred to as the entry point. The sink
also has a dictionary of parameters to search. From the entry point, the module searches up
through the code to determine how the parameters are passed to the sink. Since this tool is
for an x86 64-bit system, this logic tracks parameters passed on registers as well as the stack.
In the case of static values, addresses, or local variables other than pointers, the module adds
these parameters to the sources list. If it is determined that the values were passed to the
sink as function parameters, those parameters are used for searching follow-on functions.
In the disassembly shown in Figure 4.2, we can see at the bottom of the figure a call to
strcpy(). For the illustration register RSI, the second parameter to strcpy(), is the focus for
25
tracking. Two lines up from the strcpy() call, is a MOV RSI, RDX. This means that RDX
holds the value that needs to be tracked. Two more lines up is a MOV RDX, qword ptr [RBP
+ local_30] instruction. The qword ptr [RBP + local_30] operand is a local variable for this
function. Toward the middle of the disassembly, qword ptr [RBP + local_30] is loaded with
RDI. This RDI indicates that it was the first parameter to the function. This means that the
second parameter to strcpy() came from the function’s parameter. This would then require
all calling functions to be checked to find the source.
When sources are not found inside a function and it is determined that the source came from
26
a function’s parameter, all the references to said function are found and those referenced
locations are searched. Searching the referenced locations is necessary to find all sources
that can be used in a sink. This method does not look at the same reference location twice
to prevent loops. Each time a new location is searched, it is added to the path that a source
took to get to the sink. This process repeats until all the sources have been found or after
searching all functions on the path.
For each source, the logic starts off by searching the functions that are on the source’s path
to the sink. Within each function, the source is checked if it was used in a call to another
function. If the source was in a call to another function, then that function would be added
to the list of functions to check. To prevent a recursive loop, functions are checked once.
Finding the source usage is performed twice. The first pass is to collect the information that
does not have any other dependencies. An example is a static value that can be calculated
without further searches. Values that have dependencies could be other sources that have
not been calculated yet. The second pass allows the source to calculate values from other
sources.
27
Figure 4.3. Sequence diagrams for buffer overflow detection
28
Sinks class. Get_Sources then returns to main(). The main() function repeats the process
until all the sinks are processed
4.7 Summary
This chapter discusses the requirements for the ODSS script as well as constraints for its
implementation. The chapter also describes how the modules of the ODSS script work
and the sequence in which they are executed. The next chapter further expounds upon the
detailed implementation of the ODSS script’s classes and methods.
29
THIS PAGE INTENTIONALLY LEFT BLANK
30
CHAPTER 5:
Implementation
This chapter details how the classes and methods perform tasks to find sinks and sources,
determine the attributes of a source, and calculate the overflow for a sink. This involves
a dissection of the code to view expected inputs and outputs for each class and method.
The full source code can be made available upon request. The implementation follows the
design concepts discussed in Chapter 4.
The chapter organizes the description of the ODSS script implementation into three sections.
The first section describes the global variables and the last two sections discuss the Sink
module and the Source module, respectively. The modules-specific sections provide a
broad level description of each module’s classes and the dependencies the modules have on
the Ghidra API. The class subsections list their constructors, parameters, methods, and a
description. The method subsections list prototype, parameter, return values, effects, and a
description. Figure 5.1 shows the nesting of the modules, classes, and methods.
31
Buffer Overflow Detection Method
Find_Sinks Get_Sources
Find_Overflows Find_Source_Usage
Set_Sources __Find_StrLen_Var
Calculate_Overflow Find_Source
Sources class
__Calculate_Storage_Size
Get_Source_Usage
__Source_Usage
__Calculate_String_Var_Len
Figure 5.1. Organization of modules, classes, and functions
32
5.1 Global Variables
The ODSS script uses the following global variables, which are used throughout the script.
33
This module depends on the following Ghidra API1
• flatAPI class
– getFirstFunction()
– getReferencesTo(address)
– getFunctionAfter(function)
• Function class
– getName()
– getEntryPoint()
• Reference class
– getToAddress()
– getFromAddress()
Parameters:
None
Methods:
Find_Sinks()
Find_Overflow(sink)
Description:
The Sink_Handler class contains two methods. The first method is Find_Sinks. This
method performs the discovery and creation of the sink objects. The second method is
Find_Overflow. This method sets up the CSV file into which the outputs will be written,
and executes the overflow calculation method for the sinks.
1http://ghidra.re/ghidra_docs/api/ [2].
34
Find_Sinks
Prototype:
Sink_Handler.Find_Sinks()
Parameters:
None.
Returns:
sink[]: A list of sink objects.
Effects:
A list of the sink objects is created.
Description:
Find_Sinks is the first core method called by the script. Fink_Sinks takes no arguments and
returns a list of sink objects. Find_Sinks uses Ghidra’s function database to search for the
known vulnerable sinks. Searching is conducted by looking at each function in the database
and comparing the function name against the known sink names. In some cases, a function
is listed twice in the database. This is handled by adding each new function encountered to a
local variable (a set data type). To avoid duplicate entries, the set is checked before each sink
is added. Once a sink is found, the references to the sink are gathered using Ghidra’s refer-
ence API. These references are then used to create the sink objects. The sink object is added
to a list that is returned at the end of the function. This is seen in the pseudocode Listing 5.1.
1 function = getFirstFunction ()
2 while function is not None:
3 # Filter out non sinks and duplicate functions
4 if function in Vuln_Sinks and not in double :
5 double .add( function )
6 refs = getReferencesTo ( entry )
7 #Get the references to this sink
8 for new_sink in refs:
9 sinks_used . append ( Sinks ( new_sink information ))
10 function = getFunctionAfter ( function )
11 return sinks_used
35
Find_Overflow
Prototype:
Sink_Handler.Find_Overflow(sink)
Parameters:
sink: Object containing information pertaining to a particular sink.
Returns:
None.
Effects:
Creates the CSV file for storing overflow results.
Description: The Find_Overflow method takes one argument, which is a sink object, and
returns nothing. The purpose of this method is to create one output CSV file for the
binary file. This method calls Sinks.Calculate_Overflow, using the sinks input object as a
parameter. This function uses the OutputFile global variable to check if the output CSV file
has been created. If it has not been created, the method creates the CSV file and changes
the OutputFile global variable to true.
Parameters:
name: The name of the sink as a string.
arguments: Dictionary of the sink’s arguments. The key is the argument’s position, the
value is how the parameter is passed (e.g., charptr1: RDI, charptr2: RSI).
sources: A list of source objects that belong to the sink. This value is an empty list by
default.
36
Methods:
Set_Sources(src[])
Calculate_Overflow(sink)
Description:
The Sinks class contains the information about a particular sink. Sinks in this case are
the reference locations that make calls to functions such as strcpy() and memmove(). The
purpose of this class is to organize the information about a sink and perform overflow
calculations based on the sink’s information.
Set_Sources
Prototype:
Sinks.Set_Sources(src[])
Parameters:
src[]: A list of source objects or an empty list.
Returns:
None.
Effects:
Appends the source list to the sink’s data type for sources.
Description:
Set_Sources takes in a list of sources and returns nothing. This method allows the source
objects to be assigned to the sink. The method updates the list of sources for a given sink.
Calculate_Overflow
Prototype:
Sink.Calculate_Overflow()
Parameters:
None.
Returns:
37
None.
Effects:
Writes the sinks and sources that can overflow a buffer to a CSV file and prints the results
to the console.
Description:
Calculate_Overflow calculates if a sink can be overflowed by an element from one of its
internal list of sources. Since this method is contained within a sink object, this method
does not need to take any arguments other than the self object. This method first checks
to see what type of sink is used. There are only two types of sinks: a two-argument sink
and a three-argument sink. If the sink contains two arguments, the sources are divided into
a source parameter list and a destination parameter list. Three-argument sinks contain a
source parameter list, a destination parameter list, and an amount parameter list (for the
sink’s integer parameter). Then the method checks each combination of sources, destina-
tions, and integer amounts parameters (used only in three-argument sinks) to determine if
they are on the same path. If the parameters are not on the same path, then those parameters
would not end up at the sink together. If they are on the same path, the parameters are
compared to determine if there is an overflow. If the maximum fill for the source parameter
is larger than the allocated size of the destination parameter, then a warning message is pro-
duced and these parameters are written to a CSV file. A warning message is produced for
sinks that require three arguments when the maximum fill of the integer amount parameter
and the source parameter are larger than the allocated size of the destination parameter. If
the allocated size of the source parameter is larger than the allocated size of the destination
parameter, then a caution message is produce and the parameters are written to a CSV file.
The three-argument case also requires the allocated size of the amount parameter to be larger
than the allocated size of the destination parameter. This can be seen in the pseudocode in
Listing 5.2.
38
1 if two argument sink:
2 for list of sources :
3 group source parameter into list
4 group destination parameters into list
5 for src in source parameter list
6 for dst in destination parameter list
7 if src is not on same path as dst:
8 continue
9 if src. maxfill > dst.size:
10 produce a warning message
11 elif src.size > dst.size:
12 produce a caution message
13
39
5.3 Source Module
The source Module consists of three classes: Source_Handler, Sources, and Source_Finder.
The Source_Handler class is responsible for coordinating the search for sources, the creation
of sources, and the discovery of the source’s usage. The Sources class is the object that
contains the information about the sources and performs a detailed analysis for each source.
The Source_Finder class is responsible for iterating through the code to discover the source
location for a given function.
• flatAPI class
– getFirstFunction()
– getReferencesTo(address)
– getFunctionAfter(function)
– getFunctionBefore(address)
• Function class
– getName()
– getEntryPoint()
– getLocalVariables()
• Reference class
– getToAddress()
– getFromAddress()
– getReferenceType()
• Listing class
– getCodeUnitAt(address)
– getCodeUnitBefore(address)
– getInstructionAt(address)
– getDataAt(address)
– getMaxAddress()
• Address Factory class
– getAddress(string)
• Address class
2http://ghidra.re/ghidra_docs/api/ [2]
40
– next()
• Instruction class
– getMaxAddress()
– getMinAddress()
– getRegister(index)
• Register class
– getParentRegister()
– getName()
• Data class
– hasStringValue()
– getValue()
– getBaseDataType()
• Variable Class
– textitgetStackOffset()
– getLength()
Parameters:
None.
Methods:
Get_Sources(sink)
Find_Source_Usage(sink)
Description:
The Source_Handler class contains two methods: Get_Sources and Find_Source_Usage.
Get_Sources performs the discovery and creation of the source objects. Find_Source_Usage
coordinates the order in which the source usages are discovered.
41
Get_Sources
Prototype:
Source_Handler.Get_Sources(sink)
Parameters:
sink: Object containing information pertaining to a particular sink.
Returns:
None.
Effects:
Appends a list of source objects into the sink object.
Description:
Get_Sources is called after all the sinks in the program have been found. Get_Sources takes
one argument, a sink object, and returns a list of source objects that belong to a particular
sink. The main purpose of Get_Sources is to coordinate the search of the source objects and
to build the list of the source objects that belong to a particular sink. This method uses an
abstract data structure similar to a stack to build a tree like path from the sink’s location to
the location that the source originated. The stack initially has one object: the sink that was
passed to the function. The sink’s information is then passed to Source_Finder.Find_Source
to find the sources in that particular function. This returns two outcomes. The first is a list
of source information that was found in the function. The source information is then used
to create source objects. These sources are then added to a list.
The second outcome is a list of parameters that still need to be found. Get_Sources then
finds all the references to the current function and adds them to the stack. When functions
are searched, they are popped from the stack. The process of adding functions to the stack
and popping the functions that have been searched continues until the stack is empty, mean-
ing there are no more sources that can be found. This is seen in the pseudocode Listing 5.3.
42
1 stack . append (sink)
2 while stack :
3 #Call function to find sources and values that still need to be
found
4 sources , keep_searching = Find_Sources ( stack .pop ())
5 for src in sources :
6 # append source object to source list
7 source_list . append ( Sources (src information ))
8 #When elements are in keep_searching , add all the references to the
9 # current function to the stack
10 if keep_searching :
11 refs = getReferencesTo ( current_function )
12 for new_func_to_search in refs:
13 stack . append )( new_funct_to_search )
14 return source_list
The following example uses Figure 5.2. Get_Sources uses its sink parameter to get the
location of the called sink in Func1. Since not all the sources could be found in Func1,
Get_Sources finds all the calls to Func1. In this case there are two calls, one in Func2 and
one in Func3. Func2 and Func3 are then pushed on the stack. In this example, we assume
that Func2 is now at the top of the stack. This means Func2 is searched next, by popping
it from the stack. Since Func2 did not have the sources, Get_Sources then finds all the
calls to Func2. This results in finding the call to Func2 in Func4. Func4 is then pushed on
the stack. Since Func4 is on top the stack, Func4 is popped from the stack and searched
for sources. The search resulted in finding the remaining sources and Get_Sources stops
pushing additional functions on the stack. This leaves Func3 on the stack. Func3 is popped
from the stack for searching. Get_Sources is notified by Find_Sources that one source was
found and one source still needs to be found. Get_Sources then finds the call to Func3 in
Func5 and Func6. Func5 and Func6 are then pushed to the stack. The remaining sources
are found after these last two functions are searched. This causes the stack to empty because
all the sources were found.
43
Figure 5.2. Source tree with the sink as the root (repeat from chapter 3)
Find_Source_Usage
Prototype:
Source_Handler.Find_Source_Usage(sink)
Parameters:
sink: Object containing information pertaining to a particular sink.
Returns:
None.
Effects:
44
Updates the size values for the source objects.
Description:
Find_Source_Usage takes one argument, a sink object, and updates the values for the source
objects. This method coordinates the first and second pass for discovering the source’s usage.
Parameters:
None.
Methods:
Find_Sources(entry_address, entry_args, extra_index)
Description:
The purpose of the Source_Finder class is to iterate through individual functions in order
to determine the source locations. This class contains two methods: Find_Sources and
Find_StrLen_Var. Find_Sources searches the given function to discover information about
its parameters. Find_Strlen_Var looks at calls to functions such as strlen() and wcslen() to
determine the size of the parameter that was passed to these kinds of functions.
Find_Sources
Prototype:
Source_Finder.Find_Sources(entry_address, entry_args, extra_index)
Parameters:
entry_address: The address location to start searching for the sources as a Ghidra address
object.
entry_args{}: A dictionary of the arguments to find. The key is the parameter from the
sink. The value is the item (register or stack address) to find (e.g., charptr1: RDI, int: RSI).
45
extra_index[[argument, operand, value]]: A list of lists containing information used to
perform offset calculations to find the source. The outer list consists of zero to many items.
The inner list has three items: the sink’s argument where the source is used (e.g., the sink’s
destination parameter is charptr1); a string representation of the value of the operand that
contains the base plus offset location (e.g., RSI or [RBP + 0x10]); and the integer value of
the required offset.
Returns:
sources[[arg, name, instruction]]: A list of lists containing information about a source
location. The inner list has three items: the sink’s argument where the source is used (e.g.,
the sink’s destination parameter is charptr1); the name of the location or value of the source
as a string (e.g.,: [RBP+-0x40] or 0x45); and an instruction object in which the source was
found. The list may be empty if no source locations are found.
args{}: A dictionary of the sources that need to be searched in the calling functions. The
dictionary may be empty if all the arguments were found.
Effects:
Finds source information within a given function and returns the results to the calling
function.
Description:
Find_Sources finds source information inside a particular function. Find_Sources takes
three arguments: the entry address to start searching, the arguments to search, and any extra
offsets that may need to be calculated. Dictionaries are used when there is a unique key to
organize items. Lists are used when there is not a unique key. This method returns: a list of
sources found in the current function, sources that need to be found in calling functions, and
a list of extra offsets that were used in previous functions. Find_Sources consists of several
46
parts that are used to resolve the sink’s parameters to the sources: discovering the functions
parameters, handling the extra offsets from the previous function, handling the different
instructions, accounting for additional arithmetic to locate stack locations, and determining
if a source is a pointer.
Handling Extra Offsets: To handle the extra offsets from the previous function, the initial
location needs to be found. Once found, the extra offset can be used to determine the actual
47
stack location for a given source. This is commonly seen with structs, where a pointer to
a struct is passed to a function before individual elements in the struct are used. Figure
5.3 shows that only one stack location reference is used for the strcpy(). Notice the LEA
instruction and how 0x32 was added to the pointer address. This becomes the extra offset
that needs to be discovered in the functions that calls this particular function.
Figure 5.3. The use of offsets from function parameters. Left, disassembly
of the strcpy(). Right, the C source code.
Handling Instructions: In order to resolve the sources to the sinks, the search begins at the
called function. At the called function, the code is searched upwards towards the beginning
of the current function. Searching upwards allows for the discovery of the registers and
the PUSH instructions that were used in the called function. This process looks at each
instruction to determine if the instruction has information about one of the sources. For the
purpose of this thesis, the instructions of focus are the data transfer instructions (e.g., MOV,
LEA), ADD, PUSH, CALL and JMP. Each will be discussed in detail below.
Data transfer instructions: Data transfer instructions are instructions that perform some
type of reading, writing, or both for memory locations. Data transfer instructions, such as
MOV, are instructions where a value is read from one location and is written to another
location. These instructions require the most scrutiny because of the various values and
types that can be used to copy the sources. These instructions check for four cases: sources
that are passed to a function by value, sources that came from parameters, sources that
require base plus offset calculations, and the source’s originating location. This research
focused on the following data transfer instructions: MOV, MOVSX, MOVXD, MOVSS,
48
LEA, and CVTTSS2SI.
When values like structs are passed by value to a function, their values can either be moved or
pushed onto the stack. Values that are moved onto the stack require additional calculations
to determine its source information. First, it must be determined that the pass by value
occurred. This is determined from the previously called function. If the source is not a
location in the current function’s stack frame (e.g., [RBP + 0x10] is the previous function’s
stack frame) and is an address (i.e., not a pointer), then the source was passed by value.
The pass by value source will have an offset associated with it. When a function saves RBP
to the stack, the offset value is subtracted by 0x10 to account for the previous stack frame
and saved RBP register. With the adjusted offset, the pass by value source is then compared
against the offset that was used to move the source onto the stack. For example, this is seen
in instructions similar to MOV qword ptr[RAX], <immediate/register/memory>. When the
offsets are equal the source has been found. This was tested against pass by value examples
that use RAX to move values on the stack. Using RAX is based on the experiments that
were performed in the behavioral tests, which are discussed in Chapter 6. Using RAX is
not all encompassing and the register used to move values on the stack may vary depending
on the GCC compiler settings.
Table 5.1 and Table 5.2 show an example of values being moved onto the stack for parameter
passing. First, the stack is adjusted to make room for the struct. Then piece by piece the
values of the struct are moved onto the stack. The beginning of the struct is located at a
lower address compared to the end of the struct. Inside the called function, the values of
the members in the struct are referenced based on the RBP offset. To access the values,
the called function subtracts 0x10 from the RBP offset to account for the saved return and
pushed RBP. The resulting offset is compared against the calling function’s move location.
If a match is found, the particular member is the pass by value parameter. Using the LEA
RSI, <immediate/register/memory> instruction in the called function as an example, the
value [RBP + 0x18] yields a resulting offset of 0x8. This value is then compared against the
MOV instructions from the calling function. The offset from the instruction MOV qword
ptr [RAX + 0x8], RDX matches the offset from the called function. This means that the
new value to track is the value in the second operand position. In this case the value is the
RDX register.
49
Table 5.1. Instructions for a pass by value parameter
Instructions
SUB RSP, 0x20
MOV RAX, RSP
MOV RDX, Start_Of_Struct
MOV qword ptr [RAX], RDX
MOV RDX, Middle_Of_Struct1
MOV qword ptr [RAX + 0x8], RDX
MOV RDX, Middle_Of_Struct2
MOV qword ptr [RAX + 0x10], RDX
MOV RDX, End_Of_Struct
MOV qword ptr [RAX + 0x18], RDX
CALL Pass_By_Value_Function
Label Pass_By_Value_Function:
PUSH RBP
MOV RBP, RSP
LEA RDI, [RBP + 0x10] #Start_Of_Struct
LEA RSI, [RBP + 0x18] #Middle_Of_Struct1
CALL Some_Function
50
There are cases where the calling function and the called function offsets do not align. This
can occur when moving values across two consecutive struct fields. This is most often seen
when a character array field is next to another character array field. The end of one character
array is moved with the beginning of the next character array. For example, if the program
needs to move two four-byte character arrays to the stack, then the compiler may implement
one eight-byte MOV instruction to place both arrays on the stack. Two arrays were moved
to the stack, however the instruction that placed the arrays on the stack moved both of them
at once. Moving both arrays at once makes it difficult to distinguish the end of one array
from the start of the other. This can cause the called function’s offset to be different from
the calling function’s offset.
To solve this problem, we had to identify when the calling function’s and called function’s
offsets became unaligned. When searching backwards through the instructions, we find the
end of the struct before the beginning of the struct. When the calling offset becomes less
than the called offset, this indicates that we passed the beginning of the character array.
Thus, when the calling function’s offset becomes less than the called function’s offset, the
previous instruction’s offset in the calling function contains the start of the character array.
With the start of the array we have now identified the beginning of the source.
Listing 5.5 shows the ODSS script instruction flow for the data transfer instructions. The
pseudocode shows how the assembly instructions are inspected to determine how they may
affect sources.
The pseudocode starting at line 12 shows the logic to check if a register is being stored with
a source. If operand 1 is a register, then the register needs to be checked if it contains a
tracked value. If operand 1 is a tracked value, then operand 1 is checked to see if it has
a register and an offset. Similar to the extra offsets for previous functions (see handling
extra offsets subsection at the beginning of this method), base plus offset arithmetic can be
used to calculate locations within the function. The method then checks to see if the offset
arithmetic came from a parameter or a local variable. When the operand is a parameter,
the value is then added to the extra index list to be tracked in calling functions. When
the operand is a local variable, then the base plus offset arithmetic is calculated to get the
actual location of the variable. When operand 1 does not have offset arithmetic, operand 2
becomes the new tracked item.
51
1 if data_transfer_instructions :
2 # Check to see if the storing instruction is used to pass by value
3 if operand1 was passed by value :
4 if source to find location == MOV location :
5 track MOV location
6 elif MOV location < source to find location :
7 then source to find location is not stack aligned
8 Calculate actual offset
9 track calculated offset
10 # Check to see if a register is being stored with a source
11 elif operand1 is a register :
12 if register == a source to find value :
13 if source to find requires offset math for its location :
14 if source to find came from a parameter :
15 add source to find to extra_index list
16 elif source to find came from a local variable :
17 calculate actual offset
18 track calculated offset
19 # Check if the source value came from a parameter
20 if source to find value came from a parameter :
21 add to arguments list
22 #Does register require a base plus offset calculation
23 if operand2 required base plus offset calculations :
24 Calculate new offset
25 add to extra_math dictionary
26 continue
27 # Check to see if value is a source
28 if operand2 is an address , constant , or local variable :
29 add to found_sources list
30 else: #The first operand is not a register
31 if operand1 == source to find value :
32 if source to find came from a parameter :
33 add to arguments list
34 else:
35 if operand2 is an address , constant , or local variable :
36 add to found_sources list
37 else:
38 track the operand2
52
At this point a tracked value could have changed due to the previous operations. Starting on
line 22 of Listing 5.5, when the tracked value changed, it is compared against the function’s
parameters. If the tracked value came from a parameter, the tracked information is added
to the arguments list and then the program continues to the next iteration of the loop.
On line 26 of Listing 5.5, if a tracked value has a base plus offset calculation, then the
register becomes the value to track and the offset is saved for when the location is found.
When the location for the register is found the offset is added back to the register location
to get the actual source location. For this thesis only one register is used to calculate the
offset. Future work could include the use of multiple registers for calculating the offset.
On line 32 of Listing 5.5, there is a check to see if the tracked value can be added to the
source list. If the tracked value is a local variable, an address, or a constant, then the tracked
value is added to the sources list.
Line 36 of Listing 5.5 covers the case where operand 1 is not a register. In this case, operand
1 is checked to see if the operand is a tracked value. If the operand is a tracked value, then
operand 2 is checked to see if operand 2 came from a parameter. If it is a parameter the
value for operand 2 is added to the arguments list. If operand 2 is not a parameter, then
operand 2 is checked to see if it is a source. If operand 2 is a local variable, address, or
constant, then the value is added to the source list.
ADD: The ADD instruction indicates that offset calculations are used to point to specific
stack values. This is similar to the extra offsets that are passed between functions; however,
in this case the values may be local to the function. When an ADD instruction is seen,
the operands are checked to see if the instruction affects one of the sources. If a source is
affected, that source is added to a dictionary with the amount to add as the dictionary value.
This dictionary entry is used later, when the source location is discovered.
PUSH: When parameters are passed by pushing them on the stack, the order in which
they are pushed needs to be checked. In the called function, pushed values have positive
offsets with respect to RBP. In the calling function, the offset is used to calculate the order
in which the value was pushed. This can be achieved by subtracting 0x8 from the offset
value, then dividing by 0x8. Subtracting eight accounts for the saved return address. Since
the PUSH instruction on 64-bit system always pushes eight bytes, dividing by eight results
53
in a number that can be used to determine the order with which the value was pushed. The
pushes are ordered by their location in the stack. Lower stack addresses have lower push
order and vice versa. Table 5.3 shows an example of how the push order is determined.
Pushes that are closer to the function call have lower push order values. Knowing the push
order, a counter is used to keep track of the number of pushes that have been seen. When
the push orders match up, the value that was pushed becomes the new source value to track.
CALL and JMP: JMP/Jcc instructions are indicators of a change in the control flow. A
change in control flow may indicate that a source did not take the conditional path to the
sink. This problem is outside of the scope of this thesis and potential solutions are discussed
as future work in Chapter 8.
In the cases of three-parameter sinks, an integer is needed to determine how many bytes
are to be copied. This integer can come from a string length function. When the CALL
instruction transfers the execution flow to a function that calculates a string length, the size
of the string is returned as an integer. Knowing the result of the string length function
allows a better understanding of the number of bytes that can fill the sink. This relies on
the Find_StrLen_Var method from the Find_Source_Handler class to determine the integer
returned by the string length function.
Resolving pointers: Once iteration through the function to find sources is complete, the
sources are examined to determine if they are the actual source location or a pointer. This is
done by searching from the beginning of the function to find how the sources are used. If the
source is determined to be a pointer, the pointer information is passed back to Find_Source
in order to find its true source. This recursive process continues until the pointer resolves
to a location. If the pointer was not able to be resolved, then the pointer is passed on as
the source. This means the source is misidentified and may result in calculating the wrong
54
value for the source. Misidentifying the pointer is discussed further in Chapter 7.
Find_Strlen_Var
Prototype:
Source_Handler.__Find_Strlen_Var(entry, value, str_len)
Parameters:
entry: Ghidra address object of the location to start searching.
value: The stack location that is used to calculate the size of a character array in the string
length function. This value is passed as a string data type.
str_len: The name of the string length function (e.g., strlen()). This value is passed as a
string data type.
Returns:
String: Returns a string that contains an equation for solving the size of a given source. If
an equation cannot be determined, then an empty string is returned.
Effects:
Calculates the value from functions that return a string’s length.
Description:
Find_Strlen_Var is responsible for determining the equation that is used in functions such
as strlen() and wcslen(). This method returns an equation as a string, which is used later
once the size of the stack location is determined. This equation is used for determining the
maximum fill value for the sink’s integer amount parameter. This method starts at the call
to the string length function. The method then iterates down through the code to determine
the arithmetic that is used on the RAX register (the string length function returns its result in
the RAX register). This is performed until the value is loaded into the RSI or RDI register.
Registers RSI and RDI indicate that the size of the string is used as the integer parameter
at a sink. For this thesis, this method can only solve addition and multiplication problems.
Reversing equations that go beyond the use of addition and multiplication is left for future
work. This method is not intended to solve multiple uses of string length functions. For
example, this will not solve the case of calculating the length of two strings and then adding
55
them together.
The following example illustrates the use of Find_Strlen_Var. Listing 5.6 shows a sink that
uses the wcslen() function. Figure 5.4 shows the disassembly of the C source code. The
call to wcslen() takes in the stack location [RBP + -0x40]. Wcslen() returns the result in
the RAX register. Memcpy() needs the number of bytes, not the wide character length, so
RAX is multiplied by 0x4, which is the size of the wide char, to get the number of bytes
in this string. The results of the multiplication is loaded into RDX, which is then used in
memcpy().
Find_Strlen_Var starts at the call to wcslen(). The method then iterates down through the
code to determine the arithmetic that is used for the sink. In this case the final equation is
((([RBP+-0x40]/4)+0x1)*0x4). The [RBP+-0x40] is a place holder until the actual size is
determined. This location is divided by four because this program always calculates sizes
in bytes, not character types. Dividing by four accounts for the wide character type.
56
5.3.3 Sources Class
Constructor:
class Sources(sink, arg, name, function, ref_location, location, path, size = None, max_fill
= None, usage = None, extra = None)
Parameters:
sink: The name of the sink where the source is used (e.g., strcpy()). This value is a string
data type.
arg: The parameter of the sink where the source is used (e.g., charptr1). This value is a
string data type.
name: A string used as the value or stack location for the source (e.g., [RBP+-0x40] or
0x44).
ref_location: A Ghidra address object where the source was called in the sink.
path[[function, address]]: A list of lists that contains the path from the sink to the source
location. The inner list consists of a Ghidra function object and a Ghidra address object.
size: An integer to represent the allocated size of the source. This value is zero by default.
max_fill: An integer to represent the largest fill size of the source. This value is zero by
default.
usage[]: A list of instructions as strings that the source was used. This value is an empty
list by default.
extra: An equation as a string for calculating the true size of the source. This is used for
integer parameters. This value is an empty string by default.
Methods:
__Calculate_Storage_Size(function)
57
Get_Source_Usage()
__Source_Usage(start_address, search_term)
__Calculate_String_Var_Len(function, var_to_find)
Description:
The Sources class contains the information about a particular source. Sources are the
locations/values that are used as parameters in the sinks. The purpose of this class is to
search the binary file to determine the sources use and update the information about the
source.
Calculate_Storage_Size
Prototype:
Sources.__Calculate_Storage_Size(function)
Parameters:
function: A Ghidra function object that is used to identify the search location.
Returns:
size: Returns an integer that is used to update the value for the source’s allocated size.
Effects:
This function updates the object’s name, location, function, path, and max_fill values.
Description:
Calculate_Storage_Size is used to obtain the first estimate of the allocated size of the sources.
This method estimates the size using information about each source and its surrounding
variables.
The method starts by looking at the source’s name to check if it is an address or a value.
Addresses and values have a similar format: both start with 0x. If the argument for the
source is an integer, then values starting with 0x are converted to decimal and the method
returns the decimal value as the size. If the value is contained within brackets, then the
source is an address, in which case, the method then goes to that address to determine its
size. If the value at the location is a string, then the size of the string is returned. If the
58
value is not initialized, then the references to this location are found. If the reference is a
write instruction, then the value to be written is returned for the sources size.
If the source is a local variable, then the adjacent local variables are used to calculate the
size. A source is identified as a local variable if it has a stack offset as its name. To estimate
the local variable’s size, the source’s offset is compared against the adjacent variable offset.
The adjacent local variables are gathered using Ghidra’s API. The difference between the
two offsets becomes the initial estimate for the variable’s size. Figure 5.4 shows an example
stack layout for the local variables. Ghidra produces local variables for every referenced
stack location. The first offset is typically a pointer to the stack canary. The remainder of the
offsets are variables used by the function. [RBP - 0x10] is eight bytes in length. This could
mean that the variable is a pointer, string, or even a double. All we know is that eight bytes
of space is available for use. The next variable is at [RBP - 0x20]. This makes 16 bytes of
space between the current variable and previous variable, which becomes the estimate of
the size of the source. This is only an estimate because there are cases where unallocated
structs take up the entire space between variables, but does not indicate the struct’s data
types. This 16-byte gap could be four-character arrays of four bytes. With the small amount
of evidence thus far, we can only assume that the variable used the entire space.
Get_Source_Usage
Prototype:
Sources.Get_Source_Usage()
Parameters:
None.
Returns:
59
None.
Effects:
This function updates the values for the size and max_fill values for the source.
Description:
Get_Source_Usage controls the search order for the discovery of a source’s information. The
method starts at the location the source was discovered and iterates to every function that uses
the source. The actual searching of the function is performed by Sources.__Source_Usage.
This method updates the source’s information when a string longer than the current longest
string is written to the source.
Being able to find the string length of the sources is important for determining if overflows
are possible. Figure 5.5 is used as an example for how the source’s string length plays a
factor in calculating overflows. The figure shows the addition of Func7 and the filling of
par2 in Func3. In the case of Func7, we see the need to track where the source is used on
the path to the sink. If Func7 was not checked, then par2 would appear to be an empty
string. With the information from Func7, we can see that par2 contains 19 ‘As.’ Thus the
maximum number of characters that will be copied at the sink is 20 to include the null byte.
This example scenario will not cause an overflow at the sink, and if par2 was filled with 20
‘As,’ there would be an overflow.
When looking at Func3, we see that par2 is filled with nine ‘As’ with an implied null byte.
If we were to only compare the allocated sizes of the sources, we would see that there would
60
be a false positive from par1 of Func5 (size 10) and par2 of Fun3 (size 20). When we
look at the string length for par2, we notice that this does not cause an overflow at the sink,
because par2 is only filled with 10 characters including the null byte.
Figure 5.5. Source tree highlighting how the sources are filled
61
Source_Usage
Prototype:
Sources.__Source_Usage(start_address, search_term)
Parameters:
start_address: A Ghidra address object where the search for the source usage starts.
search_term: A string representation of the value that needs to be found inside the function.
Returns:
Called_func[[function_address, track, instruction_address]]: A list of lists that contains the
function calls where the source is used as a parameter. The inner list consists of an address
object of the function call, a string representation of the value to track, and an address object
containing the location where the CALL instruction was called. This method may return
an empty list if the source is not used in a CALL instruction.
Effects:
The method finds information for the sources and updates each source’s values based on the
findings. It also finds function calls where the sources are used as a parameter.
Description:
Source_Usage searches an individual function for the use of a particular source. This
method starts searching from the start_address location looking for every instance that the
source is used. This is similar to Find_Sources: it tracks information that is moved around
in registers. If the parameter is used in a CALL instruction, then the information about the
called function is added to a list. Calls to functions such as memset() and wmemset() are
checked inside Source_Usage to determine the fill amount of the source. By finding the
third parameter to memset() or wmemset(), which tells these functions how many bytes to
write to the source, we can calculate the number of bytes written to the source. Knowing
the number of bytes written tells us the string length for the source.
Calculate_String_Var_Len
Prototype:
Sources.__Calculate_String_Var_Len(func, var_to_find)
62
Parameters:
func: The function where the size and max_fill for a source are calculated. This is a Ghidra
function object.
var_to_find: A string representation of the value that needs to be found inside the function.
Returns:
None.
Returns:
This method updates the size and max_fill values for the source.
Description:
Calculate_String_Var_Len performs a more in depth estimate of the allocated size and
string length of the source. This method returns nothing, but it does update the source’s
maximum fill and allocated size information. This method determines the size of a variable
in a single function. This method is best suited for initialized variables because the method
looks at the information that is written to the stack. Uninitialized variables may not have
information written to their location until they are used in another function. In this case,
information about the uninitialized variables will not be on the stack, which prohibits the
ability to determine the string length of the uninitialized source. Although this method does
a better job of estimating the source’s size information, the algorithm may not accurately
estimate the size of the source. This is further discussed in Chapter 7.
The method first finds the fill information of the source. When a local variable is initialized
on the stack, its value is moved using a series of operations into the stack memory allocated
for that variable. Initializing variables occurs in two ways. First, by moving the bytes
individually into the variable’s stack location. Second, by using a loop to move values into
the variables stack location. When moving bytes individually, the values are moved using
the largest segments first, followed by smaller segments. When variables are initialized
with a loop, there needs to be a consistent byte value that is moved (e.g., the charater ‘a’ or
the null byte). This is seen in the case of initializing large arrays with the null byte.
To determine the initialized size of the variable, the null byte helps to identify the end of
the string, although, in some cases, the null byte may not be present. In those cases, we can
63
estimate the initialized values that are moved individually by observing where a change in
the move sizes occurs. For example, if four bytes were moved followed by eight bytes, it is
a good indication that a string ended at the four-byte move. The four-byte move instruction
is then checked to see how many bytes were actually moved. When there are three bytes
in operand 2, then there may be an implicit null byte at the end to indicate the end of the
string. This method does not work for every situation, but it does serve to provide a better
estimate of the fill size. An example where this method will not work is when two eight-byte
variables are next to each other and are not null terminated. This will cause the variables to
look like a string longer than eight bytes.
Figure 5.6 shows the disassembly of a local variable being initialized on the stack. This is
an example of a character array of 50 bytes being initialized to contain the 26-byte alphabet.
In this case, eight bytes are moved into the first three stack locations. Then, the final two
bytes are moved into the following location. The remainder of the bytes are null bytes,
which are used to fill out the remainder of the array.
The preceding steps calculates the allocated size and the largest string length of the source.
64
With the largest string length of the source, the method then resolves the equation that
was created by the Find_StrLen_Var method for this particular source. The largest string
length of the source is then replaced with the variable in the equation. This then allows the
equation to be solved and the result of the equation is then used as the largest size of the
source. Note that this is only applied to integer parameters.
5.4 Summary
This chapter has built upon the design presented in Chapter 4 by providing detailed infor-
mation about the implementation of our tool. This involved taking a deeper look into the
classes and methods that are implemented inside of the Sink and Source modules. Next
we describe the behavioral testing, the purpose of which is to demonstrate how Ghidra
was tested to inform the development of the script and the functional test which is used to
evaluate the effectiveness of the script.
65
THIS PAGE INTENTIONALLY LEFT BLANK
66
CHAPTER 6:
Test Plan
The test plan consists of two parts. First, the behavioral test verifies how Ghidra can analyze
the binary file. The behavioral test is aimed at understanding outputs and syntax Ghidra
produces from the disassembly. Second is the functional test for determining the accuracy
of the detection method. The functional test utilizes a sample of programs from the Juliet
test suite [3].
67
because the source’s size calculation is performed by counting the bytes at the location of
the source. This means the differences between wide or normal characters will not skew
the tests. The grouping of overflow variant types, flow variant types, and the number of
parameters at the sink forms a hierarchy which can be seen in Figure 6.1.
The Juliet test suite follows a standard naming convention for its files. The first part is the
CWE ID followed by the shortened CWE entry name. This is proceeded by the functional
variant name and the flow variant. The functional variant name is considered the flaw type
68
and the flow variant is a number to represent the control flow used in the program (refer to
Appendix B for the flow variant list). The last identifier is the programming language.
Example:
CWE Entry ID: 121
Shortened CWE Entry Name: Stack Based Buffer Overflow
Functional Variant: CWE805_char_declare_memcpy
Flow Variant: 07
Language: C
This creates the file:
CWE121_Stack_Based_Buffer_Overflow__CWE805_char_declare_memcpy_07.c
69
Table 6.2. Summary description of the test cases
Test Overflow Variant Number of Flow Vari- Sink
case Parameters ant
19 CWE805 3 53 strncpy()
20 CWE805 3 65 wcsncpy()
21 CWE806 3 11 wcsncpy()
22 CWE806 3 22 strncpy()
23 CWE806 3 32 memcpy() (wide char)
24 CWE806 3 34 wcsncat()
25 CWE806 3 42 strncat()
26 CWE806 3 54 memmove()
27 CWE806 3 66 memcpy()
28 Destination declared 2 17 strcat()
29 Destination declared 2 31 wcscat()
30 Destination declared 2 34 wcscpy()
31 Destination declared 2 41 strcpy()
32 Destination declared 2 51 wcscat()
33 Destination declared 2 67 strcat()
34 Source declared 2 18 wcscpy()
35 Source declared 2 21 wcscat()
36 Source declared 2 32 strcpy()
37 Source declared 2 34 strcat()
38 Source declared 2 42 wcscat()
39 Source declared 2 54 strcat()
40 Source declared 2 68 strcpy()
Table 6.2. The test cases came from the Juliet test suite in the CWE 121
Stack Based Buffer Overflow category [3]
70
the binary file is loaded into Ghidra, where Ghidra runs its analysis on the binary. Then, a
manual inspection of the disassembly is conducted. Based on the disassembly information,
the sinks are evaluated to see if they could be overflowed.
The manual inspection involves locating the sinks and all the sources for the sinks. This
requires identifying the space allocated on the stack and the largest string length for each
source. This is compared against the source file to determine if there are differences
between the source file’s allocated space and the disassembly’s allocated space. Differences
can occur for various reasons with the most common reason involving alignment. If an
array is 14 bytes in the source code, the compiler may align the array to the stack. When this
happens, the compiler could allocate 16 bytes on the stack to make the array stack-aligned.
This essentially makes the array 16 bytes in length. Once all the source values have been
determined, they are compared against each another to determine if an overflow is possible.
Listing 6.1 shows the vulnerable source code for test case 15. The manual inspection
involves identifying the sinks, sources, and calculating the overflow. In this case, there is
one sink: strncat(). The function’s input parameters are a pointer named data which points
to a 50-byte buffer, a char array named source which is 100 bytes in size, and an integer of
100. We can also see that the source parameter is filled with 99 C’s from memset() and is
then null terminated. Based on the source code, the destination parameter is going to be
overflowed by 50 bytes.
71
Figure 6.2 shows the vulnerable disassembly for test case 15. This figure shows the stack
layout as well as the decompiled code for the test case. In this case, the destination parameter
originates from local_b8 and the source parameter originates from local_78. In both the
stack layout and the decompiled code, we can observe that local_b8 contains 64 bytes and
local_78 contains 99 bytes (the null byte gets its own variable). Based on the disassembly
there is an overflow of 36 bytes.
Table 6.3 shows the summary of the expected results from the tests based on the manual
inspection. To eliminate any confusion between the disparity between the source code and
disassembly, Table 6.3 shows the overflow amount as seen in the source code and as seen
in the disassembly. Ultimately, determination of whether a buffer overflow exists depends
upon the disassembly, not the source code. Thus, the expected results column shows the
program’s outcome based on the disassembly. The expected results column shows the ex-
pected number of messages, the expected type of message, and the expected function name
in which the overflow is expected to occur. In the Juliet test cases, functions are marked
good or bad depending on their vulnerability at the sink. There are cases where functions
labeled good can produce caution messages. In these cases, the function does not change
the allocated size of the buffers, but it does reduce the string length in the buffer. The details
72
of each case can be seen in Appendix G.
73
Test Overflow Source Disassembly Expected results
Case Variant code overflow
overflow amount
amount
One warning in bad function
22 CWE 806 50 bytes 50 bytes
Two cautions in good function
One warning in bad function
23 CWE 806 200 bytes 200 bytes
One caution in good function
One warning in bad function
24 CWE 806 200 bytes 200 bytes
One caution in good function
One warning in bad function
25 CWE 806 50 bytes 50 bytes
One caution in good function
One warning in bad function
26 CWE 806 50 bytes 50 bytes
One caution in good function
One warning in bad function
27 CWE806 50 bytes 50 bytes
One caution in good function
28 Destination 50 bytes 36 bytes One warning in bad function
declared
29 Destination 200 bytes 192 bytes One warning in bad function
declared
30 Destination 200 bytes 192 bytes One warning in bad function
declared
31 Destination 50 bytes 0 bytes No overflow
declared
32 Destination 200 bytes 0 bytes No overflow
declared
33 Destination 50 bytes 0 bytes No overflow
declared
One warning in bad function
34 Source de- 200 bytes 192 bytes
One caution in good function
clared
74
Test Overflow Source Disassembly Expected results
Case Variant code overflow
overflow amount
amount
One warning in bad function
35 Source de- 200 bytes 192 bytes
Two cautions in good function
clared
One warning in bad function
36 Source de- 50 bytes 36 bytes
One caution in good function
clared
One warning in bad function
37 Source de- 50 bytes 36 bytes
One caution in good function
clared
One warning in bad function
38 Source de- 200 bytes 192 bytes
One caution in good function
clared
One warning in bad function
39 Source de- 50 bytes 50 bytes
One caution in good function
clared
One warning in bad function
40 Source de- 50 bytes 50 bytes
One caution in good function
clared
Table 6.3. Results of the manual inspection of the source code and disas-
sembly
When the destination parameter allocated space is less than the maximum string length
of the source parameter, a warning message is produced and the information about the
sources is displayed. In the case of the three-parameter sinks, the script stipulates that the
75
number of bytes to read must be larger than the destination parameter’s allocated space. A
caution message is produced when the destination parameter’s allocated space is less than
the allocated space of the source parameter. If a warning message is produced for a given
set of sources, no caution message is produced for the same sources.
True positives are counted when a caution or warning message is produced for a sink that
is vulnerable to a buffer overflow. A false positive is counted when a caution or warning
message is produced when it is not possible for the sink to be overflowed. A false negative
is counted when no caution or warning message is produced for a sink that is vulnerable to
a buffer overflow. Since actual stack-allocated sizes and the sizes listed in the source code
may differ, the differences in byte calculations are not subtracted from the error rates of the
predictions.
76
CHAPTER 7:
Testing Results
The behavioral test gave valuable insight into how Ghidra handles functions and variables.
Correctly identifying variable locations is crucial for determining the exact location of the
sources. The information gained from this test was used in the implementation of the buffer
overflow detection method. The functional testing revealed the feasibility of using this
buffer overflow detection method to determine vulnerable sinks. This testing also identified
areas where the detection method can improve and identified a limitation in locating sources.
The first time a reference is made to a stack frame location, Ghidra would create a local
variable for that reference. The local variable would contain the variable’s size and a list
of references to that variable. The size indicates the amount that was read from or written
to the variable. If the size is four, this may indicate a four-byte integer. If the size is
one, then this may indicate a one-byte character. If there is a gap between two variables,
this may indicate that the lower offset variable could be an array or an uninitialized struct.
Understanding how Ghidra assigns variables on the stack aided in calculating the sizes of
the variables.
77
7.2 Functional Tests
The functional tests showed that tracking the sources to sinks is a viable method for detecting
overflows caused by vulnerable sinks. Comparing the expected and observed results, the
buffer overflow detection method was successful for 95% of the test cases. Table 7.1 shows
a summary of the test results. Full test results are in Appendix H. In most test cases the
detection method was able to accurately estimate the allocated sizes and fill sizes of the
sources. The primary causes of deviations in the size estimate were due to accounting for
or not accounting for a null byte. The two cases that resulted in errors were due to the
incorrect identification of a null byte from a good implementation of a sink and the inability
to identify a source due to pointer usage.
Table 7.1. Summary of the results from the buffer overflow detection method
Test Case Expected results Actual results TP FP FN F-Score
1 1W 1W 1 0 0 1
2 1W 3W 1 2 0 .5
3 1W 1W 1 0 0 1
4 2C 2C 2 0 0 1
5 1W 1W 1 0 0 1
6 0W 0W 0 0 0 N/A*
7 0W 0W 0 0 0 N/A*
8 0W 0W 0 0 0 N/A*
9 1W 1W 1 0 0 1
10 1W 2W** 1 1 0 .67
11 1W 1W 1 0 0 1
12 0W 0W 0 0 0 N/A*
13 0W 0W 0 0 0 N/A*
14 0W 0W 0 0 0 N/A*
15 1W 1W 1 0 0 1
16 1W 1W 1 0 0 1
17 1W 1W 1 0 0 1
18 0W 0W 0 0 0 N/A*
19 0W 0W 0 0 0 N/A*
78
Table 7.1. Summary of the results from the buffer overflow detection method
Test Case Expected results Actual results TP FP FN F-Score
20 0W 0W 0 0 0 N/A*
21 1W, 2C 1W, 2C 3 0 0 1
22 1W, 2C 1W, 2C 3 0 0 1
23 1W, 1C 1W, 1C 2 0 0 1
24 1W, 1C 1W, 1C 2 0 0 1
25 1W, 1C 1W, 1C 2 0 0 1
26 1W, 1C 1W ,1C 2 0 0 1
27 1W, 1C 1W, 1C 2 0 0 1
28 1W 1W 1 0 0 1
29 1W 1W 1 0 0 1
30 1W 1W 1 0 0 1
31 0W 0W 0 0 0 N/A*
32 0W 0W 0 0 0 N/A*
33 0W 0W 0 0 0 N/A*
34 1W, 1C 1W, 1C 2 0 0 1
35 1W, 2C 1W, 2C 3 0 0 1
36 1W, 1C 1W, 1C 2 0 0 1
37 1W, 1C 1W, 1C 2 0 0 1
38 1W, 1C 1W, 1C 2 0 0 1
39 1W, 1C 1W, 1C 2 0 0 1
40 1W, 1C 1W, 1C 2 0 0 1
Total 27W, 19C 30W, 19C 46 3 0 .97
W = Warning message
C = Caution message
* = F-score calculation resulted in all zeros
** = Identified the wrong source
79
Figure 7.1. Error in Test Case 2 caused by null byte
80
a four-byte variable that resides immediately after local_68 (local_2c requires four bytes
because local_68 is a UTF-32 wide char array). Since there is a direct reference to local_2c,
Ghidra treats it as an independent variable, although this null byte value is part of the char
array. Thus, with local_2c the actual size of the array is 64 bytes. This can be fixed by
adding extra checks for the presence of a null bytes after a char array.
Test Case 10 produced two warnings when it should have produced one (see Listing 7.1).
This is the result of the source location being misidentified as a pointer. Figure 7.2 shows
the disassembly that caused the detection method to misidentify the source. Starting at the
memmove() sink at the bottom of the figure, the source can be tracked up through code by
following the last use of the operand. We see that the source is first at a pointer at offset
-0x28. The value in offset -0x28 then came from the pointer at offset -0x38. Offset -0x38
is then loaded with the address of offset -0x48. Offset -0x48 is a pointer, not a source
location. Since there is no instruction to load offset -0x48, the detection method did not
find the actual value with which -0x48 is associated. The actual value of offset -0x48 is a
pointer to offset -0x1d. This is the result of offset -0x30 pointing to offset -0x48 and then
offset -0x1d is loaded into the location to which offset -0x30 points.
81
19 printLine (data);
20 }
21 }
22 }
There is no simple solution for this case. This would require extensive analysis of pointer
usage within functions to determine the actual source. Analysis like this would be better
suited for dynamic analysis because the values that the pointers point to would need to be
checked when they are used. This is needed because the values that the pointers point to
can be different depending on the control flow that the program took.
Figure 7.2. Incorrect source identification due to pointer use. The left is
the original disassembly. The right is the marked-up version.
82
7.3 Discussion
The tool focuses on function-to-function interactions, testing how sources pass through
different functions to end up at a sink. Based on the testing results, tracking sources through
parameter passing was demonstrated to be possible. Tracking sources inside a function
illuminated some weaknesses. The test results showed that the tool needs a more robust
means for tracking pointer usage to correctly identify sources. Other areas for improvements
include improving the accuracy of the source size calculations, properly identifying data
types in uninitialized structs, and determining sources from multiple register usage.
1 int main ()
2 {
3 char string1 [50];
4 char string2 [50];
5
83
Figure 7.3. Disassembly of the stack layer for the source test
An example of direct referencing into a variable is seen in Listing 7.2 and Figure 7.3.
Listing 7.2 shows a direct reference in the middle of a character array via static assignment
within a loop. Figure 7.3 shows the stack layout that Ghidra produced from the binary
code. Note that local_88 refers to string1 and local_48 refers to string2. For string1, we
see a variable at location local_7b, which is the 13th index in the array. Using the current
source size calculation method, the difference between local_88 and local_7b would result
in the incorrect size of 13 for the variable. Without the source code, further investigation is
needed to detect these cases.
This problem does not have a good solution. One possible method would be to identify as
many clues about the struct as possible. The information from functions like sizeof() make
for good clues when calculating the length of a data type. This type of function sometimes
84
forces the compiler to statically assign the size value of the data type as an integer in the
binary file. The statically assigned integer values allow for a direct means of understanding
the space that a variable was allocated.
85
THIS PAGE INTENTIONALLY LEFT BLANK
86
CHAPTER 8:
Conclusion
Overall, the ODSS script demonstrated that static analysis of binary programs could lead
to the discovery of buffer overflow vulnerabilities. The bottom-up tracking process showed
how to use the sinks and sources found in binary code to discover potential vulnerabilities.
This method proved to be successful against a number of test cases in the Juliet test suite.
The selected Juliet test cases were useful for testing variations of a sink’s usage; however,
those programs do not present the same challenges that exist in real world applications.
One example is that the test cases relied on one register for indexing into the stack. Larger
programs rely on multiple registers to index into a stack location, which is commonly
the case for arrays. This chapter summarizes the motivation and results of this work and
discusses potential enhancements to address the limitations of the current implementation.
8.1 Summary
The motivation for this work is to improve software assurance for the DoD by applying static
analysis on binary code to determine the robustness of a program against buffer overflows.
The research shows that it is possible to automate the discovery and testing of vulnerable
sinks in a binary file for buffer overflows. The sink’s parameters were tracked through the
code to find the parameter’s source locations. With the source locations, discovery of a
source’s usage was used to help determine the allocated size of the source and the largest
string for that source. Once all the sources were calculated, the sources were compared
against one another to determine if a buffer overflow was possible. This was implemented
using the Ghidra API for searching, iterating, and referencing various aspects of the binary
file. The final result was a Ghidra script written in Python to discover buffer overflows in
the binary file being examined by Ghidra. Testing was conducted on vulnerable programs
found inside of the Juliet test suite’s stack-based overflow test cases. This method was
successful for the Juliet test suite programs with an overall F-score of .97.
87
8.2 Future Work
This research aimed to demonstrate that the proposed method of tracking sinks and sources
is feasible. Future work would address control flow inside functions, detecting overflows
in variadic functions, testing with multiple types of compilers and compiler options, and
providing additional information to the user. Work on these topics will allow our detection
method to discover more overflows in the code and improve the range of programs that the
ODSS script can produce useful results. Adding more vulnerable sinks to the test corpus
would allow the script to discover more overflows. Using multiple types of compilers would
allow the script to be used on more programs.
Control flow inside a function can be tracked in a manner similar to the function-to-function
control flow, by building a tree to the source information. Instead of looking at references to
the function calls, the tool would need to look at the different references to address locations
caused by execution control instructions (e.g., JMP or Jcc instructions). To achieve this,
each address needs to be checked for reference locations. Those reference locations become
the nodes of the path tree and the search for sources is continued for each node. This
method works for cases where there are direct references to a location. Cases like a JMP
RAX would not work because the value for RAX is not known. Static analysis would not be
the best options because it may not be possible to calculate register values. Thus, dynamic
analysis would be better suited to handling this type of control flow.
88
parameters to use. This format specifier is the starting point for discovering possible buffer
overflows. For strings, variadic functions rely on the null byte to indicate the end of the
string to be read. To limit the number of characters to read, a format specifier can use the
width sub-specifier to specify the number of characters to read.
To implement a detection method for these functions, the format specifier must be parsed.
The value from the format specifier becomes the input amount for that parameter. Once the
parameter’s width sub-specifier is calculated, the search for the sources can begin. Without
a sub-specifier, the method would mark the function as an overflow risk, because there is
no mechanism to prevent the buffer from being overflowed. The variadic-aware method
operates the same as the current detection method by searching the code from the sinks to
find the sources. Once the sources have been found, their allocated sizes can be compared
against their respective format specifier. If the specifier is larger than the allocated size of
the parameter, then an overflow is possible.
89
8.2.5 User Feedback
The tool gives feedback on the sink and source information that it was able to find. There are
cases where the tool makes estimates for the source’s size and location. When an estimate
needs to be used, information pertaining to the estimate could be displayed to the user.
This would allow the user to help verify if the estimate is correct or to identify the correct
information to use. There are cases where the values will not be known until run time. If
run time information is needed, a message can be displayed to the user. Such messages
would let the user know which sinks will need to be looked at via dynamic analysis.
90
APPENDIX A:
Implement a Ghidra Script
Ghidra has a well-developed API for performing various tasks on the disassembled infor-
mation. The API allows users to write their own scripts to search, edit, or comment on
disassembled information. The scripts can be written in Java or Python to utilize the API.
This appendix describes how to implement a Ghidra script and run the script on a binary
file.
First, the script manager needs to be opened. This can be done by opening a binary file and
clicking on Window, then scrolling down to the script manager icon. The script manager
icon can also be accessed from the top tool bar shown in Figure A.1.
Once opened the script manager’s menu will appear. On the left side of the script manager
is a list of the script folders. These folders are not file locations; instead they are categories
into which the scripts belong. Categorization is based on the first few comments in the
script file. At the top right there are two main buttons of interest. The first is the create
script button. This will allow the user to select a Java or Python file to begin writing a
script. It will also ask for the directory the file should be stored in. This will then open a
text editor, allowing the user to begin writing code. The second button is the directory list.
This allows the user to add directories for Ghidra to use when looking for script files. This
is seen in Figure A.2.
When a script is selected, the file can be viewed and edited by clicking on the basic editor
button. Inside the script, the top few comments determine what Ghidra will display for the
script and how it is categorized. Comments starting with “@category:” determine where
the file is stored in the script folders. For example, if “@category” is “@Bufferoverflow
Detection”, the script will be stored in the “Bufferoverflow Detection” folder.
To run a scripts, the script must be selected. Then, the green run script button will appear,
which if selected, will run the script. This is seen in Figure A.3.
91
Figure A.1. Ghidra menu for opening the script menu
92
Figure A.2. Ghidra script manager window
Figure A.3. Ghidra script manager for view, editing, and running scripts
93
THIS PAGE INTENTIONALLY LEFT BLANK
94
APPENDIX B:
Test Case Flow Variant
This appendix describes the flow variants implemented in the Juliet test cases used for this
work. Table B.1 was taken from the Juliet test suite [3].
• Flow variants 1-18: Uses conditional to control the flow of the program.
• Flow variants 21 & 22: Uses conditional to control the flow of the program and passes
source information between functions.
• Flow variants 31 & 32: Uses pointers to point to the source values.
• Flow variant 34: Uses a union for the source parameter.
• Flow variants 41, 42, 44, & 45: Passes source information between functions.
• Flow variants 51-54: Passes source values between files.
• Flow variants 61-68 Pass values to different functions, but pass different types of
information.
95
Flow Flow Description C C++
Variant Type
08 Control if(staticReturnsTrue()) and X X
if(staticReturnsFalse())
09 Control if(GLOBAL_CONST_TRUE) and X X
if(GLOBAL_CONST_FALSE)
10 Control if(globalTrue) and if(globalFalse) X X
11 Control if(globalReturnsTrue()) and X X
if(globalReturnsFalse())
12 Control if(globalReturnsTrueOrFalse()) X X
13 Control if(GLOBAL_CONST_FIVE==5) and X X
if(GLOBAL_CONST_FIVE!=5)
14 Control if(globalFive==5) and if(globalFive!=5) X X
15 Control switch(6) and switch(7) X X
16 Control while(1) X X
17 Control for loops X X
18 Control goto statements X X
21 Control Flow controlled by value of a static global vari- X X
able. All functions contained in one file.
22 Control Flow controlled by value of a global variable. X X
Sink functions are in a separate file from sources.
31 Data Data flow using a copy of data within the same X X
function
32 Data Data flow using two pointers to the same value X X
within the same function
33 Data Use of a C++ reference to data within the same * X
function
34 Data Use of a union containing two methods of ac- X X
cessing the same data (within the same function)
41 Data Data passed as an argument from one function X X
to another in the same source file
96
Flow Flow Description C C++
Variant Type
42 Data Data returned from one function to another in X X
the same source file
43 Data Data flows using a C++ reference from one func- * X
tion to another in the same source file
44 Control/ Data passed as an argument from one function X X
Data to a function in the same source file called via a
function pointer
45 Data Data passed as a static global variable from one X X
function to another in the same source file
51 Data Data passed as an argument from one function X X
to another in different source files
52 Data Data passed as an argument from one function X X
to another to another in three different source
files
53 Data Data passed as an argument from one function X X
through two others to a fourth; all four functions
are in different source files
54 Data Data passed as an argument from one function X X
through three others to a fifth; all five functions
are in different source files
61 Data Data returned from one function to another in X X
different source files
62 Data Data flows using a C++ reference from one func- * X
tion to another in different source files
63 Data Pointer to data passed from one function to an- X X
other in different source files
64 Data void pointer to data passed from one function to X X
another in different source files
97
Flow Flow Description C C++
Variant Type
65 Control/ Data passed as an argument from one function X X
Data to a function in a different source file called via
a function pointer
66 Data Data passed in an array from one function to X X
another in different source files
67 Data Data passed in a struct from one function to X X
another in different source files
68 Data Data passed as a global variable in the “a” class X X
from one function to another in different source
files
72 Data Data passed in a vector from one function to * X
another in different source files
73 Data Data passed in a linked list from one function to * X
another in different source files
74 Data Data passed in a hash map from one function to * X
another in different source files
81 Data Data passed in an argument to a virtual function * X
called via a reference
82 Data Data passed in an argument to a virtual function * X
called via a pointer
83 Data Data passed to a class constructor and destructor * X
by declaring the class object on the stack
84 Data Data passed to a class constructor and destructor * X
by declaring the class object on the heap and
deleting it after use
Table B.1. Flow Variants for the Juliet test suite [3]
98
APPENDIX C:
Compilation
The Juliet test programs are compiled using the GCC configurations listed in Appendix D.
The programs contain macros to specify pieces of code that are or not part of the compilation
process. These programs are compiled individually, which means each program requires a
main function. These programs are also compiled with the GCC option -fno-builtin. This
prevents the sinks from being inlined to the functions.
The header files std_testcases.h and std_testcases_io.h need to be in the same directory as
the program files. These header files contain variables and macro definitions that are used
in the test programs. The C program io.c is also needed in the compilation process. The
program io.c handles the various print functions that are used in the test case programs.
The examples below use files from the Juliet test suite [3].
In the cases of multiple files, each file will be included in the compilation process.
Multiple file compilation example:
gcc \DINCLUDEMAIN -fno-builtin io.c
CWE121_Stack_Based_Buffer_Overflow__dest_char_declare_cpy_54a.c
CWE121_Stack_Based_Buffer_Overflow__dest_char_declare_cpy_54b.c
CWE121_Stack_Based_Buffer_Overflow__dest_char_declare_cpy_54c.c
CWE121_Stack_Based_Buffer_Overflow__dest_char_declare_cpy_54d.c
CWE121_Stack_Based_Buffer_Overflow__dest_char_declare_cpy_54e.c
-o CWE121_Stack_Based_Buffer_Overflow__dest_char_declare_cpy_54
99
THIS PAGE INTENTIONALLY LEFT BLANK
100
APPENDIX D:
GCC Compiler Options
This Appendix lists the settings that were used by the GNU GCC compiler [32]. Multiple
GCC commands were used to obtain the current settings.
Output:
gcc (Ubuntu 8.3.0-6ubuntu1) 8.3.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Output:
The following options are target specific:
-m128bit-long-double [enabled]
-m16 [disabled]
-m32 [disabled]
-m3dnow [disabled]
-m3dnowa [disabled]
-m64 [enabled]
-m80387 [enabled]
-m8bit-idiv [disabled]
-m96bit-long-double [disabled]
-mabi= sysv
-mabm [disabled]
-maccumulate-outgoing-args [disabled]
-maddress-mode= long
-madx [disabled]
-maes [disabled]
-malign-data= compat
101
-malign-double [disabled]
-malign-functions= 0
-malign-jumps= 0
-malign-loops= 0
-malign-stringops [enabled]
-mandroid [disabled]
-march= x86-64
-masm= att
-mavx [disabled]
-mavx2 [disabled]
-mavx256-split-unaligned-load [enabled]
-mavx256-split-unaligned-store [enabled]
-mavx5124fmaps [disabled]
-mavx5124vnniw [disabled]
-mavx512bitalg [disabled]
-mavx512bw [disabled]
-mavx512cd [disabled]
-mavx512dq [disabled]
-mavx512er [disabled]
-mavx512f [disabled]
-mavx512ifma [disabled]
-mavx512pf [disabled]
-mavx512vbmi [disabled]
-mavx512vbmi2 [disabled]
-mavx512vl [disabled]
-mavx512vnni [disabled]
-mavx512vpopcntdq [disabled]
-mbionic [disabled]
-mbmi [disabled]
-mbmi2 [disabled]
-mbranch-cost=<0,5> 3
-mcall-ms2sysv-xlogues [disabled]
-mcet-switch [disabled]
-mcld [disabled]
-mclflushopt [disabled]
102
-mclwb [disabled]
-mclzero [disabled]
-mcmodel= [default]
-mcpu=
-mcrc32 [disabled]
-mcx16 [disabled]
-mdispatch-scheduler [disabled]
-mdump-tune-features [disabled]
-mf16c [disabled]
-mfancy-math-387 [enabled]
-mfentry [disabled]
-mfma [disabled]
-mfma4 [disabled]
-mforce-drap [disabled]
-mforce-indirect-call [disabled]
-mfp-ret-in-387 [enabled]
-mfpmath= sse
-mfsgsbase [disabled]
-mfunction-return= keep
-mfused-madd
-mfxsr [enabled]
-mgeneral-regs-only [disabled]
-mgfni [disabled]
-mglibc [enabled]
-mhard-float [enabled]
-mhle [disabled]
-miamcu [disabled]
-mieee-fp [enabled]
-mincoming-stack-boundary= 0
-mindirect-branch-register [disabled]
-mindirect-branch= keep
-minline-all-stringops [disabled]
-minline-stringops-dynamically [disabled]
-mintel-syntax
-mlarge-data-threshold=<number> 65536
103
-mlong-double-128 [disabled]
-mlong-double-64 [disabled]
-mlong-double-80 [enabled]
-mlwp [disabled]
-mlzcnt [disabled]
-mmemcpy-strategy=
-mmemset-strategy=
-mmitigate-rop [disabled]
-mmmx [enabled]
-mmovbe [disabled]
-mmovdir64b [disabled]
-mmovdiri [disabled]
-mmpx [disabled]
-mms-bitfields [disabled]
-mmusl [disabled]
-mmwaitx [disabled]
-mno-align-stringops [disabled]
-mno-default [disabled]
-mno-fancy-math-387 [disabled]
-mno-push-args [disabled]
-mno-red-zone [disabled]
-mno-sse4 [enabled]
-mnop-mcount [disabled]
-momit-leaf-frame-pointer [disabled]
-mpc32 [disabled]
-mpc64 [disabled]
-mpc80 [disabled]
-mpclmul [disabled]
-mpcommit [disabled]
-mpconfig [disabled]
-mpku [disabled]
-mpopcnt [disabled]
-mprefer-avx128
-mprefer-vector-width= none
-mpreferred-stack-boundary= 0
104
-mprefetchwt1 [disabled]
-mprfchw [disabled]
-mpush-args [enabled]
-mrdpid [disabled]
-mrdrnd [disabled]
-mrdseed [disabled]
-mrecip [disabled]
-mrecip=
-mrecord-mcount [disabled]
-mred-zone [enabled]
-mregparm= 6
-mrtd [disabled]
-mrtm [disabled]
-msahf [disabled]
-msgx [disabled]
-msha [disabled]
-mshstk [disabled]
-mskip-rax-setup [disabled]
-msoft-float [disabled]
-msse [enabled]
-msse2 [enabled]
-msse2avx [disabled]
-msse3 [disabled]
-msse4 [disabled]
-msse4.1 [disabled]
-msse4.2 [disabled]
-msse4a [disabled]
-msse5
-msseregparm [disabled]
-mssse3 [disabled]
-mstack-arg-probe [disabled]
-mstack-protector-guard-offset=
-mstack-protector-guard-reg=
-mstack-protector-guard-symbol=
-mstack-protector-guard= tls
105
-mstackrealign [disabled]
-mstringop-strategy= [default]
-mstv [enabled]
-mtbm [disabled]
-mtls-dialect= gnu
-mtls-direct-seg-refs [enabled]
-mtune-ctrl=
-mtune= generic
-muclibc [disabled]
-mvaes [disabled]
-mveclibabi= [default]
-mvect8-ret-in-mem [disabled]
-mvpclmulqdq [disabled]
-mvzeroupper [enabled]
-mwbnoinvd [disabled]
-mx32 [disabled]
-mxop [disabled]
-mxsave [disabled]
-mxsavec [disabled]
-mxsaveopt [disabled]
-mxsaves [disabled]
106
(for use with the -mindirect-branch=/-mfunction-return= options):
keep thunk thunk-extern thunk-inline
Known data alignment choices (for use with the -malign-data= option):
abi cacheline compat
Known vectorization library ABIs (for use with the -mveclibabi= option):
acml svml
Output:
The following options control optimizations:
-O<number>
-Ofast
-Og
-Os
107
-faggressive-loop-optimizations [enabled]
-falign-functions [disabled]
-falign-functions= 1
-falign-jumps [disabled]
-falign-jumps= 1
-falign-labels [disabled]
-falign-labels= 1
-falign-loops [disabled]
-falign-loops= 1
-fassociative-math [disabled]
-fasynchronous-unwind-tables [enabled]
-fauto-inc-dec [enabled]
-fbranch-count-reg [disabled]
-fbranch-probabilities [disabled]
-fbranch-target-load-optimize [disabled]
-fbranch-target-load-optimize2 [disabled]
-fbtr-bb-exclusive [disabled]
-fcaller-saves [disabled]
-fcode-hoisting [disabled]
-fcombine-stack-adjustments [disabled]
-fcompare-elim [disabled]
-fconserve-stack [disabled]
-fcprop-registers [disabled]
-fcrossjumping [disabled]
-fcse-follow-jumps [disabled]
-fcx-fortran-rules [disabled]
-fcx-limited-range [disabled]
-fdce [enabled]
-fdefer-pop [disabled]
-fdelayed-branch [disabled]
-fdelete-dead-exceptions [disabled]
-fdelete-null-pointer-checks [enabled]
-fdevirtualize [disabled]
-fdevirtualize-speculatively [disabled]
-fdse [enabled]
108
-fearly-inlining [enabled]
-fexceptions [disabled]
-fexpensive-optimizations [disabled]
-ffast-math
-ffinite-math-only [disabled]
-ffloat-store [disabled]
-fforward-propagate [disabled]
-ffp-contract=[off|on|fast] fast
-ffp-int-builtin-inexact [enabled]
-ffunction-cse [enabled]
-fgcse [disabled]
-fgcse-after-reload [disabled]
-fgcse-las [disabled]
-fgcse-lm [enabled]
-fgcse-sm [disabled]
-fgraphite [disabled]
-fgraphite-identity [disabled]
-fguess-branch-probability [disabled]
-fhandle-exceptions
-fhoist-adjacent-loads [disabled]
-fif-conversion [disabled]
-fif-conversion2 [disabled]
-findirect-inlining [disabled]
-finline [enabled]
-finline-atomics [enabled]
-finline-functions [disabled]
-finline-functions-called-once [disabled]
-finline-small-functions [disabled]
-fipa-bit-cp [disabled]
-fipa-cp [disabled]
-fipa-cp-clone [disabled]
-fipa-icf [disabled]
-fipa-icf-functions [disabled]
-fipa-icf-variables [disabled]
-fipa-profile [disabled]
109
-fipa-pta [disabled]
-fipa-pure-const [disabled]
-fipa-ra [disabled]
-fipa-reference [disabled]
-fipa-sra [disabled]
-fipa-vrp [disabled]
-fira-algorithm=[CB|priority] CB
-fira-hoist-pressure [enabled]
-fira-loop-pressure [disabled]
-fira-region=[one|all|mixed] [default]
-fira-share-save-slots [enabled]
-fira-share-spill-slots [enabled]
-fisolate-erroneous-paths-attribute [disabled]
-fisolate-erroneous-paths-dereference [disabled]
-fivopts [enabled]
-fjump-tables [enabled]
-fkeep-gc-roots-live [disabled]
-flifetime-dse [enabled]
-flifetime-dse=<0,2> 2
-flimit-function-alignment [disabled]
-flive-range-shrinkage [disabled]
-floop-interchange [disabled]
-floop-nest-optimize [disabled]
-floop-parallelize-all [disabled]
-floop-unroll-and-jam [disabled]
-flra-remat [disabled]
-fmath-errno [enabled]
-fmodulo-sched [disabled]
-fmodulo-sched-allow-regmoves [disabled]
-fmove-loop-invariants [disabled]
-fnon-call-exceptions [disabled]
-fnothrow-opt [disabled]
-fomit-frame-pointer [disabled]
-fopt-info [disabled]
-foptimize-sibling-calls [disabled]
110
-foptimize-strlen [disabled]
-fpack-struct [disabled]
-fpack-struct=<number>
-fpartial-inlining [disabled]
-fpatchable-function-entry=
-fpeel-loops [disabled]
-fpeephole [enabled]
-fpeephole2 [disabled]
-fplt [enabled]
-fpredictive-commoning [disabled]
-fprefetch-loop-arrays [enabled]
-fprintf-return-value [enabled]
-freciprocal-math [disabled]
-freg-struct-return [enabled]
-frename-registers [enabled]
-freorder-blocks [disabled]
-freorder-blocks-algorithm=[simple|stc] simple
-freorder-blocks-and-partition [disabled]
-freorder-functions [disabled]
-frerun-cse-after-loop [disabled]
-freschedule-modulo-scheduled-loops [disabled]
-frounding-math [disabled]
-frtti [enabled]
-fsched-critical-path-heuristic [enabled]
-fsched-dep-count-heuristic [enabled]
-fsched-group-heuristic [enabled]
-fsched-interblock [enabled]
-fsched-last-insn-heuristic [enabled]
-fsched-pressure [disabled]
-fsched-rank-heuristic [enabled]
-fsched-spec [enabled]
-fsched-spec-insn-heuristic [enabled]
-fsched-spec-load [disabled]
-fsched-spec-load-dangerous [disabled]
-fsched-stalled-insns [disabled]
111
-fsched-stalled-insns-dep [enabled]
-fsched-stalled-insns-dep=<number>
-fsched-stalled-insns=<number>
-fsched2-use-superblocks [disabled]
-fschedule-fusion [enabled]
-fschedule-insns [disabled]
-fschedule-insns2 [disabled]
-fsection-anchors [disabled]
-fsel-sched-pipelining [disabled]
-fsel-sched-pipelining-outer-loops [disabled]
-fsel-sched-reschedule-pipelined [disabled]
-fselective-scheduling [disabled]
-fselective-scheduling2 [disabled]
-fshort-enums [enabled]
-fshort-wchar [disabled]
-fshrink-wrap [disabled]
-fshrink-wrap-separate [enabled]
-fsignaling-nans [disabled]
-fsigned-zeros [enabled]
-fsimd-cost-model=[unlimited|dynamic|cheap] unlimited
-fsingle-precision-constant [disabled]
-fsplit-ivs-in-unroller [enabled]
-fsplit-loops [disabled]
-fsplit-paths [disabled]
-fsplit-wide-types [disabled]
-fssa-backprop [enabled]
-fssa-phiopt [disabled]
-fstack-check=[no|generic|specific]
-fstack-clash-protection [disabled]
-fstack-protector [disabled]
-fstack-protector-all [disabled]
-fstack-protector-explicit [disabled]
-fstack-protector-strong [disabled]
-fstack-reuse=[all|named\_vars|none] all
-fstdarg-opt [enabled]
112
-fstore-merging [disabled]
-fstrict-aliasing [disabled]
-fstrict-enums [disabled]
-fstrict-volatile-bitfields [enabled]
-fthread-jumps [disabled]
-fno-threadsafe-statics [enabled]
-ftracer [disabled]
-ftrapping-math [enabled]
-ftrapv [disabled]
-ftree-bit-ccp [disabled]
-ftree-builtin-call-dce [disabled]
-ftree-ccp [disabled]
-ftree-ch [disabled]
-ftree-coalesce-vars [disabled]
-ftree-copy-prop [disabled]
-ftree-cselim [enabled]
-ftree-dce [disabled]
-ftree-dominator-opts [disabled]
-ftree-dse [disabled]
-ftree-forwprop [enabled]
-ftree-fre [disabled]
-ftree-loop-distribute-patterns [disabled]
-ftree-loop-distribution [disabled]
-ftree-loop-if-convert [enabled]
-ftree-loop-im [enabled]
-ftree-loop-ivcanon [enabled]
-ftree-loop-optimize [enabled]
-ftree-loop-vectorize [disabled]
-ftree-lrs [disabled]
-ftree-parallelize-loops=<number> 1
-ftree-partial-pre [disabled]
-ftree-phiprop [enabled]
-ftree-pre [disabled]
-ftree-pta [disabled]
-ftree-reassoc [enabled]
113
-ftree-scev-cprop [enabled]
-ftree-sink [disabled]
-ftree-slp-vectorize [disabled]
-ftree-slsr [disabled]
-ftree-sra [disabled]
-ftree-switch-conversion [disabled]
-ftree-tail-merge [disabled]
-ftree-ter [disabled]
-ftree-vectorize
-ftree-vrp [disabled]
-funconstrained-commons [disabled]
-funroll-all-loops [disabled]
-funroll-loops [disabled]
-funsafe-math-optimizations [disabled]
-funswitch-loops [disabled]
-funwind-tables [enabled]
-fvar-tracking [enabled]
-fvar-tracking-assignments [enabled]
-fvar-tracking-assignments-toggle [disabled]
-fvar-tracking-uninit [disabled]
-fvariable-expansion-in-unroller [disabled]
Specifies the cost model for vectorization.
-fvect-cost-model=[unlimited|dynamic|cheap] [default]
-fvpt [disabled]
-fweb [enabled]
-fwrapv [disabled]
-fwrapv-pointer [disabled]
114
APPENDIX E:
Behavioral Test
To utilize Ghidra’s API, it was necessary to understand how Ghidra handled parameters
that were passed as registers and how Ghidra identified local variables. A large part of this
thesis revolved around the ability to track variables back to their source. This was tricky
with 64-bit operating systems because functions pass the first six parameters in registers and
the remaining parameters are passed on the stack. Listing E.1 is the test program we used
to see how Ghidra handled the parameters as they were passed to different types of functions.
11 struct struct_test
12 {
13 char test1 [50];
14 char test2 [100];
15 char test3 [50];
16 char test4 [20];
17 int test5 ;
18 };
19
20 int global_test ;
21 char global_test2 ;
22 void test1_no_par ();
23 void test2_one_par (int par1);
24 void test3_two_par (int par1 , int par2);
25 void test4_two_par_ptr (char *par1 , char *par2);
115
26 void test5_seven_par (int par1 , int par2 , char *par3 , char *par4 , char *
par5 , char *par6 , char *par7);
27 void test6_one_par_call (int par1);
28 void test7_two_par_call (char *par1 , int par2);
29 void test8_seven_par_call (int par1 , int par2 , char *par3 , char *par4 ,
char *par5 , char *par6 , char *par7);
30 void test9_struct_init ( struct struct_test *);
31 void test10_struct_uninit ( struct struct_test *);
32 void test11_struct_by_value ( struct struct_test struct1 );
33 void test12_casting (char charpar , int ipar , float fpar);
34
35 int main ()
36 {
37 int ipar1 = 10;
38 int ipar2 = 20;
39 int ipar3 = 30;
40 float fpar1 = 10.1;
41 char charpar1 = ’a’;
42 char cpar1 [] = {" Hello World "};
43 char cpar2 [50] = {" abcdefghijklmnopqrstuvwxyz "};
44 char cpar3 [] = {" 123456789 "};
45 char cpar4 [] = {"test test test"};
46 char cpar5 [] = {"as; lfjaslkdfnalkfhaslk "};
47 struct struct_test struct1 = {"", "", "", "", 0};
48 struct struct_test struct2 ;
49 struct struct_test struct3 = {"TEST TEST", "TEST TEST", "TEST TEST", "
TEST TEST", 4};
50
51 test1_no_par ();
52 test2_one_par ( ipar1 );
53 test3_two_par (ipar1 , ipar2 );
54 test4_two_par_ptr (cpar1 , cpar2 );
55 test5_seven_par (ipar1 , ipar2 , cpar1 , cpar2 , cpar3 , cpar4 , cpar5 );
56 test6_one_par_call ( ipar3 );
57 test7_two_par_call (cpar1 , ipar2 );
58 test8_seven_par_call (ipar1 , ipar2 , cpar1 , cpar2 , cpar3 , cpar4 , cpar5 );
59 test9_struct_init (& struct1 );
60 test10_struct_uninit (& struct2 );
61 test11_struct_by_value ( struct3 );
62 test12_casting (charpar1 , ipar1 , fpar1 );
116
63
64 }
65
66 void test1_no_par ()
67 {
68 char cpar1 [] = {" Hello test1 "};
69 printf ("%s\n", cpar1 );
70 }
71
90 void test5_seven_par (int par1 , int par2 , char *par3 , char *par4 , char *
par5 , char *par6 , char *par7)
91 {
92 int lvar1 = par1;
93 int lvar2 = par2;
94 char lvar3 = par3 [0];
95 char lvar4 = par4 [0];
96 char lvar5 = par5 [0];
97 char lvar6 = par6 [0];
98 char lvar7 = par7 [0];
99 }
100
117
102 {
103 int total = par1 + 5;
104 printf ("%i\n", total );
105 }
106
114 void test8_seven_par_call (int par1 , int par2 , char *par3 , char *par4 ,
char *par5 , char *par6 , char *par7)
115 {
116 int lvar1 = par1;
117 int lvar2 = par2;
118 char lvar3 = par3 [0];
119 char lvar4 = par4 [0];
120 char lvar5 = par5 [0];
121 char lvar6 = par6 [0];
122 char lvar7 = par7 [0];
123 printf ("%i %i %s %s %s %s %s", lvar1 , lvar2 , par3 , par4 , par5 , par6 ,
par7);
124 }
125
118
140 printf ("%s", struct1 -> test3 );
141 printf ("%s", struct1 -> test4 );
142 printf ("%d", struct1 -> test5 );
143 strcpy (struct1 ->test4 , struct1 -> test3 );
144 }
145
173 }
Listing E.1: Source Code for testing Ghidra’s decomiler on assembly code
119
Figure E.1. Test 1: Function with no parameters
Test 1 was to observe the output from a function that took in no parameters. In Figure E.1
we see that Ghidra successfully identified that no parameters were passed to the function
test1_no_par.
120
Figure E.2. Test 2: Function with one parameter
Test 2 passed one parameter to the function. This function performed a simple addition
using the parameter and a local variable. This function ultimately did nothing because the
sum was stored in a local variable and was not returned to the previous function. Figure
E.2 shows two interesting aspects about Ghidra. First, Ghidra recognized the parameter as
a local variable. This may be in part because the compiler stored the content of the registers
as local variables; however, later we will see that other functions will label parameters
differently. Second, the decompiler recognized that the function did nothing, so an empty
function was the result of the decompilation.
121
Figure E.3. Test 3: Function with two parameters
Test 3 passed two parameters to a function with no content. Figure E.3 shows that Ghidra’s
decompiler recognized that the function did nothing, and it did not list the two parameters
that were passed to the function.
122
Figure E.4. Test 4: Function with two parameters passed as pointers
Test 4 passed two parameters as pointers to the function. This function wrote the content
of the two parameters to the same global variable. Figure E.4 shows that the disassembler
recognized the parameters as local variables, yet the decompiler recognized the parameters
as parameters. We also notice that the decompiler recognized that the first assignment to
the global variable was overwritten by the second assignment. So, the decompiler only
displayed the second assignment in the decompiled view.
123
Figure E.5. Test 5: Function with seven parameters
Test 5 passed seven parameters to the function. This test was meant to observe how
Ghidra would recognize the seventh parameter that was passed on the stack. The function
itself assigned the parameters as local variables, which is the equivalent of doing nothing
since nothing is returned. Figure E.5 shows that Ghidra recognized all the parameters as
parameters, as seen as param_1 through param_7. We also notice that the decompiler
recognized that the function did nothing and produced an empty function.
124
Figure E.6. Test 6: Function with one parameter and a function call
Test 6 passed one parameter to the function that computes a sum and makes one call to
printf(). Our intent for this function was to see how Ghidra would recognize the parameter
that was stored in the function before it was used to printf(). Figure E.6 shows that the
decompiler summarized the sum part of the function and put the sum directly into the
printf() statement. Also, we see that the decompiler recognized that the parameter was used
versus a local variable.
125
Figure E.7. Test 7: Function with two parameters and a function call
Test 7 passed two parameters to a function that calls printf() from local variables. As was
the case for Test 6, Figure E.7 shows that the decompiler optimized the function by only
passing the parameters into printf(). The assembly code shows that the parameters are
moved into local variables before they are used in the printf() function.
126
Figure E.8. Test 8: Function with seven parameters and a function call
Test 8 passed seven parameters to a function that calls printf() using only one of the
parameters. Figure E.8 shows that Ghidra recognized the parameters as parameters in the
disassembly code. At the bottom of the figure we see that param_1 and param_2 is used to
pass printf() its values. In this case param_1 represents RDI and param_2 represents RSI.
The decompiler recognized that only one parameter is used in the function. In this case the
decompiler labeled the parameter as param_1 and passed that parameter into printf(). This
gets a little convoluted because the assembly code shows param_2 as the variable going
into printf() and the decompiler shows param_1 going into printf(). In the end, it is the
same value going into the printf(), but Ghidra mixed up the symbols when comparing the
assembly to the decompiled code.
127
Figure E.9. Test 9: Function with initialized struct pointer
Test 9 passed an initialized stuct pointer to a function that used printf() and strcpy(). Figure
E.9 showed how internal struct data types are passed as parameters. At the beginning of
the disassembly, it is seen that the pointer to the struct is stored in a local variable. Just
before the strcpy() call, the struct pointer is then calculated for each data type that is used
as a pointer.
128
Figure E.10. Test 10: Function with uninitialized struct pointer
Test 10 passes an uninitialized struct pointer to a function that uses printf() and strcpy().
Figure E.10 shows that uninitialized structs are handled the same way as initialized structs.
129
Figure E.11. Test 11: Function with struct passed by value
Test 11 passed a struct by values on the stack to a function that used printf() and strcpy().
Figure E.11 shows the disassembly and how structs passed by values on the stack are used
in functions. Looking just before the strcpy() call, it is noted that the parameters are directly
referenced in the stack. This is seen by the positive offsets to RBP for the parameter passed
to strcpy(). Figure E.12 shows how parameters are put on the stack. In this case the values
are pushed onto the stack; however, depending on the stack layout these values may be
moved instead of pushed onto the stack.
130
Figure E.12. Test 11: Function that calls with a struct passed by value
131
Figure E.13. Test 12: Function with casting
Test 12 passed a char, an int, and a float to a function that cast the parameters to local
variables and used printf(), strcpy(), and strncpy(). Figure E.13 shows the various ways that
casting is handled. These cases show that casting is performed by individual instructions,
which store the result as the proper data type.
132
APPENDIX F:
Script Outputs
This appendix describes the outputs produced by the buffer overflow script. The first part
covers the outputs to the console. The second part covers the outputs to the CSV files.
Warning! The source max fill size is larger than the allocated
size for the destination.
There is an over flow of: 49
Source Info
Sink: strncat
Argument: charptr1
Name: [RBP + -0xb0]
Funcion: CWE121_Stack_Based_Buffer_Overflow
__CWE806_char_declare_ncat_42_bad
Ref Location: 0010182a
Var Location: 0010181d
Path: [[CWE121_Stack_Based_Buffer_Overflow
133
__CWE806_char_declare_ncat_42_bad, 0010182a]]
Size: 50
Max Fill: 99
Usage: [’0010182a CALL 0x00101090 using RDI’,
’0010182a CALL 0x00101090 using RDI’]
Extra: None
Source Info
Sink: strncat
Argument: charptr2
Name: [RBP + -0x70]
Funcion: CWE121_Stack_Based_Buffer_
Overflow__CWE806_char_declare_ncat_42_bad
Ref Location: 0010182a
Var Location: 0010179b
Path: [[CWE121_Stack_Based_Buffer_Overflow
__CWE806_char_declare_ncat_42_bad, 0010182a]]
Size: 104
Max Fill: 99
Usage: [’001017b0 CALL 0x0010174e using RDI’,
’0010180e CALL 0x00101050 using RDI’,
’0010182a CALL 0x00101090 using RSI’,
’0010183d CALL 0x00101205 using RDI’,
’00101852 CALL 0x00101060 using qword ptr [RBP + -0xb8]’,
’0010122b CALL 0x00101070 using RSI’,
’0010176b CALL 0x00101080 using RDI’,
’001017b0 CALL 0x0010174e using RDI’,
’0010180e CALL 0x00101050 using RDI’,
’0010182a CALL 0x00101090 using RSI’,
’0010183d CALL 0x00101205 using RDI’,
’00101852 CALL 0x00101060 using qword ptr [RBP + -0xb8]’,
’0010122b CALL 0x00101070 using RSI’,
’0010176b CALL 0x00101080 using RDI’]
Extra: None
Source Info
Sink: strncat
134
Argument: int
Name: [RBP + -0x70]
Funcion: CWE121_Stack_Based_Buffer_Overflow
__CWE806_char_declare_ncat_42_bad
Ref Location: 0010182a
Var Location: 0010180e
Path: [[CWE121_Stack_Based_Buffer_Overflow
__CWE806_char_declare_ncat_42_bad, 0010182a]]
Size: 104
Max Fill: 99
Usage: [’001017b0 CALL 0x0010174e using RDI’,
’0010180e CALL 0x00101050 using RDI’,
’0010182a CALL 0x00101090 using RSI’,
’0010183d CALL 0x00101205 using RDI’,
’00101852 CALL 0x00101060 using qword ptr [RBP + -0xb8]’,
’0010122b CALL 0x00101070 using RSI’,
’0010176b CALL 0x00101080 using RDI’,
’001017b0 CALL 0x0010174e using RDI’,
’0010180e CALL 0x00101050 using RDI’,
’0010182a CALL 0x00101090 using RSI’,
’0010183d CALL 0x00101205 using RDI’,
’00101852 CALL 0x00101060 using qword ptr [RBP + -0xb8]’,
’0010122b CALL 0x00101070 using RSI’,
’0010176b CALL 0x00101080 using RDI’]
Extra:
135
Path: [[goodG2B, 00101935]]
Size: 50
Max Fill: 49
Usage: [’00101935 CALL 0x00101090 using RDI’,
’00101935 CALL 0x00101090 using RDI’]
Extra: None
Source Info
Sink: strncat
Argument: charptr2
Name: [RBP + -0x70]
Funcion: goodG2B
Ref Location: 00101935
Var Location: 001018a6
Path: [[goodG2B, 00101935]]
Size: 104
Max Fill: 49
Usage: [’001018bb CALL 0x00101859 using RDI’,
’00101919 CALL 0x00101050 using RDI’,
’00101935 CALL 0x00101090 using RSI’,
’00101948 CALL 0x00101205 using RDI’,
’0010195d CALL 0x00101060 using qword ptr [RBP + -0xb8]’,
’0010122b CALL 0x00101070 using RSI’,
’00101876 CALL 0x00101080 using RDI’,
’001018bb CALL 0x00101859 using RDI’,
’00101919 CALL 0x00101050 using RDI’,
’00101935 CALL 0x00101090 using RSI’,
’00101948 CALL 0x00101205 using RDI’,
’0010195d CALL 0x00101060 using qword ptr [RBP + -0xb8]’,
’0010122b CALL 0x00101070 using RSI’,
’00101876 CALL 0x00101080 using RDI’]
Extra: None
Source Info
Sink: strncat
Argument: int
Name: [RBP + -0x70]
136
Funcion: goodG2B
Ref Location: 00101935
Var Location: 00101919
Path: [[goodG2B, 00101935]]
Size: 104
Max Fill: 49
Usage: [’001018bb CALL 0x00101859 using RDI’,
’00101919 CALL 0x00101050 using RDI’,
’00101935 CALL 0x00101090 using RSI’,
’00101948 CALL 0x00101205 using RDI’,
’0010195d CALL 0x00101060 using qword ptr [RBP + -0xb8]’,
’0010122b CALL 0x00101070 using RSI’,
’00101876 CALL 0x00101080 using RDI’,
’001018bb CALL 0x00101859 using RDI’,
’00101919 CALL 0x00101050 using RDI’,
’00101935 CALL 0x00101090 using RSI’,
’00101948 CALL 0x00101205 using RDI’,
’0010195d CALL 0x00101060 using qword ptr [RBP + -0xb8]’,
’0010122b CALL 0x00101070 using RSI’,
’00101876 CALL 0x00101080 using RDI’]
Extra:
ghidra_overflow_detection.py> Finished!
137
Figure F.1. CSV file containing the sources that can overflow a source
138
APPENDIX G:
Functional Test Cases
This appendix summarizes the test cases from the Juliet test suite that are used for this thesis.
Each test case is described in terms of a functional variant, a sink, a flow variant, a character
type, and an expected result. The functional variant3 is the type of vulnerability in the code.
The sink is the libc function that can cause an overflow. The flow variant4 is the type of
control flow that occurs in the program. The character type is the size of the characters
used in the sources. A char is a 1-byte character and a wide is a 4-byte character. The
expected result is the type of message that the script should produce based on the assembly
code. The test cases are named based on the program name found in the Juliet test suite.
The Juliet test suite follows a standard naming convention for its files. The first part is the
CWE ID followed by the shortened CWE entry name. This is proceeded by the functional
variant name and the flow variant. The functional variant name is considered the flaw type
and the flow variant is a number to represent the control flow used in the program (refer to
Appendix B for the flow variant list). The last identifier is the programming language.
Example:
CWE Entry ID: 121
Shortened CWE Entry Name: Stack Based Buffer Overflow
Functional Variant: CWE805_char_declare_memcpy
Flow Variant: 07
Language: C
This creates the file:
CWE121_Stack_Based_Buffer_Overflow__CWE805_char_declare_memcpy_07.c
3Naming of the functional variants is based on the Juliet test suite naming convention [3].
4Naming of the flow variants is based on the Juliet test suite naming convention [3].
139
For each test case there is a table summarizing the sinks that are tested. The rows of the
table are the sinks found in the program. The columns are detailed below:
To recapitulate, if the largest string length for the source parameter is larger than the allocated
size of the destination parameter, then a warning message is produced. For the sinks that
require three arguments, the maximum fill of the integer parameter indicating the amount
of bytes to copy must be larger than the allocated size of the destination parameter. If
the allocated size of the source parameter is larger than allocated size of the destination
parameter, then a caution message is produced. The three-argument case also requires the
amount parameter to be larger than the allocated size of the destination parameter. Expected
results are based on the assembly code. The script does not look at the C code, thus it does
not produce messages for the C code. The C code information is displayed to show that
there is a different result when comparing the C code to the assembly code.
140
Test 1: CWE121_Stack_Based_Buffer_Overflow__char_type_overrun_memcpy_01
• Functional variant: Struct overrun - Incorrect length value used for a struct.
• Sink: memcpy()
• Flow variant: Uses conditional to control the flow of the program.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: No message. No overflow.
141
Test 2: CWE121_Stack_Based_Buffer_Overflow__wchar_t_type_overrun_memmove_08
• Functional variant: Struct overrun - Incorrect length value used for a struct.
• Sink: memmove()
• Flow variant: Uses conditional to control the flow of the program.
• Character type: wide char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: No message. No overflow.
– Assembly good2 function: No message. No overflow.
142
Test 3: CWE121_Stack_Based_Buffer_Overflow__CWE193_char_declare_cpy_02
143
Test 4: CWE121_Stack_Based_Buffer_Overflow__CWE193_wchar_t_declare_cpy_31
144
Test 5: CWE121_Stack_Based_Buffer_Overflow__CWE193_char_declare_cpy_34
145
Test 6: CWE121_Stack_Based_Buffer_Overflow__CWE193_wchar_t_declare_cpy_41
146
Test 7: CWE121_Stack_Based_Buffer_Overflow__CWE193_char_declare_cpy_51
147
Test 8: CWE121_Stack_Based_Buffer_Overflow__CWE193_wchar_t_declare_cpy_63
148
Test 9: CWE121_Stack_Based_Buffer_Overflow__CWE193_char_declare_memcpy_03
149
Test 10: CWE121_Stack_Based_Buffer_Overflow__CWE193_char_declare_memmove_32
150
Test 11: CWE121_Stack_Based_Buffer_Overflow__CWE193_char_declare_ncpy_34
151
Test 12: CWE121_Stack_Based_Buffer_Overflow__CWE193_wchar_t_declare_ncpy_44
152
Test 13: CWE121_Stack_Based_Buffer_Overflow__CWE193_wchar_t_declare_memcpy_52
153
Test 14: CWE121_Stack_Based_Buffer_Overflow__CWE193_wchar_t_declare_memmove_64
154
Test 15: CWE121_Stack_Based_Buffer_Overflow__CWE805_char_declare_ncat_10
• Functional variant: CWE 805 - refers to buffer access with incorrect length value.
• Sink: strncat()
• Flow variant: Uses conditional to control the flow of the program.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: No message. No overflow.
– Assembly good2 function: No message. No overflow.
155
Test 16: CWE121_Stack_Based_Buffer_Overflow__CWE805_wchar_t_declare_ncat_31
• Functional variant: CWE 805 - refers to buffer access with incorrect length value.
• Sink: wcsncat()
• Flow variant: Uses pointers to point to the source values.
• Character type: wide char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: No message. No overflow.
156
Test 17: CWE121_Stack_Based_Buffer_Overflow__CWE805_char_declare_memmove_34
• Functional variant: CWE 805 - refers to buffer access with incorrect length value.
• Sink: memmove()
• Flow variant: Uses a union for the source parameter.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: No message. No overflow.
157
Test 18: CWE121_Stack_Based_Buffer_Overflow__CWE805_wchar_t_declare_memcpy_44
• Functional variant: CWE 805 - refers to buffer access with incorrect length value.
• Sink: memcpy()
• Flow variant: Passes source information between functions.
• Character type: wide char
• Expected result:
– Assembly bad function: No message. No overflow.
– Assembly good1 function: No message. No overflow.
158
Test 19: CWE121_Stack_Based_Buffer_Overflow__CWE805_char_declare_ncat_53
• Functional variant: CWE 805 - refers to buffer access with incorrect length value.
• Sink: strncpy()
• Flow variant: Passes source values between files.
• Character type: char
• Expected result:
– Assembly bad function: No message. No overflow.
– Assembly good1 function: No message. No overflow.
159
Test 20: CWE121_Stack_Based_Buffer_Overflow__CWE805_wchar_t_declare_ncpy_65
• Functional variant: CWE 805 - refers to buffer access with incorrect length value.
• Sink: wcsncpy()
• Flow variant: Data passed as an argument from one function to a function in a
different source file called via a function pointer
• Character type: wide char
• Expected result:
– Assembly bad function: No message. No overflow.
– Assembly good1 function: No message. No overflow.
160
Test 21: CWE121_Stack_Based_Buffer_Overflow__CWE806_wchar_t_declare_ncpy_11
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: wcsncpy()
• Flow variant: Uses conditional to control the flow of the program.
• Character type: wide char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
– Assembly good2 function: Produces one caution message. Src size is greater
than Dst.
161
Test 22: CWE121_Stack_Based_Buffer_Overflow__CWE806_char_declare_ncpy_22
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: strncpy()
• Flow variant: Uses conditional to control the flow of the program and passes source
information between functions.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
– Assembly good2 function: Produces one caution message. Src size is greater
than Dst.
162
Test 23: CWE121_Stack_Based_Buffer_Overflow__CWE806_wchar_t_declare_memcpy_32
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: memcpy()
• Flow variant: Uses pointers to point to the source values.
• Character type: wide char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
163
Test 24: CWE121_Stack_Based_Buffer_Overflow__CWE806_wchar_t_declare_ncat_34
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: wcsncat()
• Flow variant: Uses a union for the source parameter.
• Character type: wide char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
164
Test 25: CWE121_Stack_Based_Buffer_Overflow__CWE806_char_declare_ncat_42
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: strncpy()
• Flow variant: Passes source information between functions.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
165
Test 26: CWE121_Stack_Based_Buffer_Overflow__CWE806_char_declare_memmove_54
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: memmove()
• Flow variant: Passes source values between files.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
166
Test 27: CWE121_Stack_Based_Buffer_Overflow__CWE806_char_declare_memcpy_66
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: memcpy()
• Flow variant: Data passed in an array from one function to another in different source
files.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
167
Test 28: CWE121_Stack_Based_Buffer_Overflow__dest_char_declare_cat_17
168
Test 29: CWE121_Stack_Based_Buffer_Overflow__dest_wchar_t_declare_cat_31
169
Test 30: CWE121_Stack_Based_Buffer_Overflow__dest_wchar_t_declare_cpy_34
170
Test 31: CWE121_Stack_Based_Buffer_Overflow__dest_char_declare_cpy_41
171
Test 32: CWE121_Stack_Based_Buffer_Overflow__dest_wchar_t_declare_cat_51
172
Test 33: CWE121_Stack_Based_Buffer_Overflow__dest_char_declare_cat_67
173
Test 34: CWE121_Stack_Based_Buffer_Overflow__src_wchar_t_declare_cpy_18
174
Test 35: CWE121_Stack_Based_Buffer_Overflow__src_wchar_t_declare_cat_21
175
Test 36: CWE121_Stack_Based_Buffer_Overflow__src_char_declare_cpy_32
176
Test 37: CWE121_Stack_Based_Buffer_Overflow__src_char_declare_cat_34
177
Test 38: CWE121_Stack_Based_Buffer_Overflow__src_wchar_t_declare_cat_42
178
Test 39: CWE121_Stack_Based_Buffer_Overflow__src_char_declare_cat_54
179
Test 40: CWE121_Stack_Based_Buffer_Overflow__src_char_declare_cpy_68
180
APPENDIX H:
Test Case Results
This appendix contains a summary of the outputs generated by the ODSS script. The test
cases show either a warning or caution message with values that were found for the given
parameters. Each test case is described in terms of a functional variant, a sink, a flow
variant, a character type, an expected result, and an actual result. The functional variant5 is
the type of vulnerability in the code. The sink is the libc function that can cause an overflow.
The flow variant6 is the type of control flow that occurs in the program. The character type
is the size of the characters used in the sources. A char is a 1-byte character and a wide is a
4-byte character. The expected result is the type of message that the script should produce
based on the assembly code. The actual result is the message(s) and sources that caused
the overflow. When a test has a buffer overflow, the results are summarized in a table. The
rows are the sources for a given sink. The columns are described below:
To recapitulate, if the largest string length for the source parameter is larger than the allocated
size of the destination parameter, then a warning message is produced. For the sinks that
require three arguments, the maximum fill of the integer parameter indicating the amount
of bytes to copy must be larger than the allocated size of the destination parameter. If
the allocated size of the source parameter is larger than allocated size of the destination
parameter, then a caution message is produced. The three-argument case also requires the
amount parameter to be larger than the allocated size of the destination parameter. Expected
and actual results are based on the assembly code. The script does not look at the C code,
thus it does not produce messages for the C code.
5Naming of the functional variants is based on the Juliet test suite naming convention [3].
6Naming of the flow variants is based on the Juliet test suite naming convention [3].
181
Test 1 Results:
• Functional variant: Struct overrun - Incorrect length value used for a struct.
• Sink: memcpy()
• Flow variant: Uses conditional to control the flow of the program.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: No message. No overflow.
• Actual results:
– Assembly bad function: Produced one warning message (Msg 1). The max fill
for arguments charptr2 and int are greater than the size of charptr1.
– Assembly good1 function: Produced no messages.
182
Test 2 Results:
• Functional variant: Struct overrun - Incorrect length value used for a struct.
• Sink: memmove()
• Flow variant: Uses conditional to control the flow of the program.
• Character type: wide char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: No message. No overflow.
– Assembly good2 function: No message. No overflow.
• Actual results:
– Assembly bad function: Produced one warning message (Msg 1). The max fill
for arguments charptr2 and int are greater than the size of charptr1.
– Assembly good1 function: Produced one warning message (Msg 2). The max
fill for arguments charptr2 and int are greater than the size of charptr1.
– Assembly good2 function: Produced one warning message (Msg 3). The max
fill for arguments charptr2 and int are greater than the size of charptr1.
Test Case 2 produced a warning for two good implementations of the sink. This is caused
by the destination parameter not accounting for the null byte at the end of the char array.
This can be fixed by adding extra checks for the presence of a null bytes after a char array.
183
Test 3 Results:
184
Test 4 Results:
185
Test 5 Results:
186
Test 6 Results:
Test 7 Results:
187
Test 8 Results:
188
Test 9 Results: Produced one warning message. Expected one warning message.
189
Test 10 Results:
Test Case 10 produced two warnings when it should have produced one. This is the result
of the source location being misidentified as a pointer. There is no simple solution for this
case; it would require extensive analysis of pointer usage within functions to determine the
actual source. Such analysis would be better suited for dynamic analysis because the values
at the pointers would need to be checked when they are used. This is needed because the
values at the pointers can be different depending on the control flow that the program took.
190
Test 11 Results:
191
Test 12 Results:
Test 13 Results:
192
Test 14 Results:
193
Test 15 Results:
• Functional variant: CWE 805 - refers to buffer access with incorrect length value.
• Sink: strncat()
• Flow variant: Uses conditional to control the flow of the program.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: No message. No overflow.
– Assembly good2 function: No message. No overflow.
• Actual results:
– Assembly bad function: Produced one warning message (Msg 1). The max fill
for arguments charptr2 and int are greater than the size of charptr1.
– Assembly good1 function: Produced no messages.
– Assembly good2 function: Produced no messages.
194
Test 16 Results:
• Functional variant: CWE 805 - refers to buffer access with incorrect length value.
• Sink: wcsncat()
• Flow variant: Uses pointers to point to the source values.
• Character type: wide char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: No message. No overflow.
• Actual results:
– Assembly bad function: Produced one warning message (Msg 1). The max fill
for arguments charptr2 and int are greater than the size of charptr1.
– Assembly good1 function: Produced no messages.
195
Test 17 Results:
• Functional variant: CWE 805 - refers to buffer access with incorrect length value.
• Sink: memmove()
• Flow variant: Uses a union for the source parameter.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: No message. No overflow.
• Actual results:
– Assembly bad function: Produced one warning message (Msg 1). The max fill
for arguments charptr2 and int are greater than the size of charptr1.
– Assembly good1 function: Produced no messages.
196
Test 18 Results:
• Functional variant: CWE 805 - refers to buffer access with incorrect length value.
• Sink: memcpy()
• Flow variant: Passes source information between functions.
• Character type: wide char
• Expected result:
– Assembly bad function: No message. No overflow.
– Assembly good1 function: No message. No overflow.
• Actual results:
– Assembly bad function: Produced no messages.
– Assembly good1 function: Produced no messages.
Test 19 Results:
• Functional variant: CWE 805 - refers to buffer access with incorrect length value.
• Sink: strncpy()
• Flow variant: Passes source values between files.
• Character type: char
• Expected result:
– Assembly bad function: No message. No overflow.
– Assembly good1 function: No message. No overflow.
• Actual results:
– Assembly bad function: Produced no messages.
– Assembly good1 function: Produced no messages.
197
Test 20 Results:
• Functional variant: CWE 805 - refers to buffer access with incorrect length value.
• Sink: wcsncpy()
• Flow variant: Data passed as an argument from one function to a function in a
different source file called via a function pointer
• Character type: wide char
• Expected result:
– Assembly bad function: No message. No overflow.
– Assembly good1 function: No message. No overflow.
• Actual results:
– Assembly bad function: Produced no messages.
– Assembly good1 function: Produced no messages.
198
Test 21 Results:
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: wcsncpy()
• Flow variant: Uses conditional to control the flow of the program.
• Character type: wide char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
– Assembly good2 function: Produces one caution message. Src size is greater
than Dst.
• Actual results:
– Assembly bad function: Produced one warning message (Msg 1). The max fill
for arguments charptr2 and int are greater than the size of charptr1.
– Assembly good1 function: Produced one caution message (Msg 2). The size
for charptr2 and int are greater than the size of charptr1.
– Assembly good2 function: Produced one caution message (Msg 3). The size
for charptr2 and int are greater than the size of charptr1.
199
Test 22 Results:
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: strncpy()
• Flow variant: Uses conditional to control the flow of the program and passes source
information between functions.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
– Assembly good2 function: Produces one caution message. Src size is greater
than Dst.
• Actual results:
– Assembly bad function: Produced one warning message (Msg 1). The max fill
for arguments charptr2 and int are greater than the size of charptr1.
– Assembly good1 function: Produced one caution message (Msg 2). The size
for charptr2 and int are greater than the size of charptr1.
– Assembly good2 function: Produced one caution message (Msg 3). The size
for charptr2 and int are greater than the size of charptr1.
200
Test 23 Results:
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: memcpy()
• Flow variant: Uses pointers to point to the source values.
• Character type: wide char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
• Actual results:
– Assembly bad function: Produced one warning message (Msg 1). The max fill
for arguments charptr2 and int are greater than the size of charptr1.
– Assembly good1 function: Produced one caution message (Msg 2). The size
for charptr2 and int are greater than the size of charptr1.
201
Test 24 Results:
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: wcsncat()
• Flow variant: Uses a union for the source parameter.
• Character type: wide char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
• Actual results:
– Assembly bad function: Produced one warning message (Msg 1). The max fill
for arguments charptr2 and int are greater than the size of charptr1.
– Assembly good1 function: Produced one caution message (Msg 2). The size
for charptr2 and int are greater than the size of charptr1.
202
Test 25 Results:
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: strncpy()
• Flow variant: Passes source information between functions.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
• Actual results:
– Assembly bad function: Produced one warning message (Msg 1). The max fill
for arguments charptr2 and int are greater than the size of charptr1.
– Assembly good1 function: Produced one caution message (Msg 2). The size
for charptr2 and int are greater than the size of charptr1.
203
Test 26 Results:
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: memmove()
• Flow variant: Passes source values between files.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
• Actual results:
– Assembly bad function: Produced one warning message (Msg 1). The max fill
for arguments charptr2 and int are greater than the size of charptr1.
– Assembly good1 function: Produced one caution message (Msg 2). The size
for charptr2 and int are greater than the size of charptr1.
204
Test 27 Results:
• Functional variant: CWE 806 - refers to buffer access using size of source buffer
• Sink: memcpy()
• Flow variant: Data passed in an array from one function to another in different source
files.
• Character type: char
• Expected result:
– Assembly bad function: Produces one warning message. Src fill and Amount
are greater than Dst.
– Assembly good1 function: Produces one caution message. Src size is greater
than Dst.
• Actual results:
– Assembly bad function: Produced one warning message (Msg 1). The max fill
for arguments charptr2 and int are greater than the size of charptr1.
– Assembly good1 function: Produced one caution message (Msg 2). The size
for charptr2 and int are greater than the size of charptr1.
205
Test 28 Results:
206
Test 29 Results:
207
Test 30 Results:
208
Test 31 Results:
Test 32 Results:
209
Test 33 Results:
210
Test 34 Results:
211
Test 35 Results:
212
Test 36 Results:
213
Test 37 Results:
214
Test 38 Results:
215
Test 39 Results:
216
Test 40 Results:
217
THIS PAGE INTENTIONALLY LEFT BLANK
218
List of References
[3] T. Boland and P. E. Black, “Juliet 1.1 C/C++ and Java Test Suite,” Computer,
vol. 45, no. 10, pp. 88–90, Oct 2012.
[4] H. Zhu, T. Dillig, and I. Dillig, “Automated inference of library specifications for
source-sink property verification,” in Asian Symposium on Programming Languages
and Systems. Springer, 2013, pp. 290–306.
[6] MITRE Corporation, “2019 CWE top 25 most dangerous software errors,” 2019.
Available: https://cwe.mitre.org/top25/archive/2019/2019_cwe_top25.html
[9] H. Booth, D. Rike, and G. Witte, “The national vulnerability database (nvd):
Overview,” National Institute of Standards and Technology, Tech. Rep., 2013.
[10] Y. Xie, A. Chou, and D. Engler, “Archer: using symbolic, path-sensitive analysis to
detect memory access errors,” ACM SIGSOFT Software Engineering Notes, vol. 28,
no. 5, pp. 327–336, 2003.
[11] D. A. Wagner, J. S. Foster, E. A. Brewer, and A. Aiken, “A first step towards auto-
mated detection of buffer overrun vulnerabilities.” in NDSS, 2000, pp. 2000–02.
[12] G. Holzmann, “Static source code checking for user-defined properties,” in Proc.
IDPT, 2002, vol. 2.
[14] G. Balakrishnan and T. Reps, “Wysinwyx: What you see is not what you execute,”
ACM Transactions on Programming Languages and Systems (TOPLAS), vol. 32,
no. 6, p. 23, 2010.
219
[15] R. Kindermann, “Static detection of buffer overflows in executables,” 2008. Avail-
able: https://www.academia.edu/3859898/Static_Detection_of_Buffer_Overflows_
in_Executables_Diplomarbeit
[17] Q. Meng, C. Feng, B. Zhang, and C. Tang, “Assisting in auditing of buffer overflow
vulnerabilities via machine learning,” Mathematical Problems in Engineering, vol.
2017, 2017.
[21] B. Knighton and C. Delikat, “Black Hat USA 2019,” Aug 2019. Available: https:
//github.com/NationalSecurityAgency/ghidra/wiki/files/blackhat2019.pdf
[22] P. E. Black, Juliet 1.3 Test Suite: Changes From 1.2. US Department of Commerce,
National Institute of Standards and Technology, 2018.
[24] MITRE, CWE, “CWE-121: Stack-based buffer overflow,” 2020. Available: https:
//cwe.mitre.org/data/definitions/121.html
220
[28] alloca(3) Linux Programmer’s Manual, March 2019. Available: http://man7.org/
linux/man-pages/man3/alloca.3.html
[30] G. Michaelson, “Bernhard Steffen, Oliver R¨uthing, and Michael Huth: Mathe-
matical foundations of advanced informatics—volume 1: Inductive approaches,”
Formal Aspects of Computing, vol. 31, no. 5, pp. 641–642, Nov 2019. Available:
https://doi.org/10.1007/s00165-019-00496-x
[31] D. R. Kuhn, R. N. Kacker, and Y. Lei, “Practical combinatorial testing,” NIST spe-
cial Publication, vol. 800, no. 142, p. 142, 2010.
[32] GCC Team et al., “GCC, the GNU Compiler Collection-GNU Project-Free Software
Foundation (FSF),” GCC, the GNU Compiler Collection.
221
THIS PAGE INTENTIONALLY LEFT BLANK
222
Initial Distribution List
223