How To Test For Buffer Overflow Vulnerabilities
Testing for buffer overflows involves the following 4 steps:
- Identify entry points.
- Craft attack data for each entry point.
- Pass attack data to each entry point.
- Look for application crashes.
1. Identify entry points
Entry points are the means by which you can provide input to the application under test. The following are common entry points:
- Public APIs
- Web service methods
- DCOM methods
- Network ports
- UI input fields
- File input (can take the form of configuration, data, or serialization information)
- Registry input
As you explore the set of entry points look for the ability to provide string input or data that may be used to calculate a buffer.
Some examples of string input entry points are:
- Web application UI input field such as username, password, or search box.
- Thick client application UI input field such as configuration options, search, or username
- Public API with a string parameter type
- String data in a file
- String data in a registry key
- String data in a network packet
There are a couple of common scenarios in which input data may be used to calculate a buffer:
- Public API that contains a parameter indicating size of a buffer parameter. For instance:
char *myCopy(byte *byteBuf, int bufSize);
- File formats that contain field lengths as part of the format.
Which in the file would like like:
- Network protocols that contain field lengths as part of the format. This is very similar to the file example shown above.
2. Craft attack data for an entry point
When crafting attack data for an input that may be used to calculate an internal buffer size you should try the following:
- Maximum size allowed by the datatype
- Minimum size allowd by the datatype
If the input is a length parameter on an API that is tied to a buffer that you are also passing to the API you should additioinally try the following:
- Size of buffer - 1
- Size of buffer + 1
If the input is a length identifier in a network protocol or file format you should additionally try the following:
- Size of existing value - 1
- Size of existing value + 1
When crafting attack data for a string input you should try the following:
- String composed entirely of the character 'a' of size 10,000.
- Random string of size 10,000 characters.
- If the application rejects due to size constraints, vary the size of the string until you understand the constraint. Try a string size exactly at the constraint, one over the constraint, and one under the constraint.
- If the application accepts the string but shows no failure symptoms increase the size of the string until you can ascertain failure or rejection based on size.
Important: If the input has to conform to a known format construct your string input around this format. Apply the 4 steps above between each delimeter of the format independently and in combination. For instance if you were testing an url format of 'http://sitename/dirname' you would do the following:
LONGSTRING://sitename/dirname http://LONGSTRING/dirname http://sitename/LONGSTRING LONGSTRING://LONGSTRING/LONGSTRING
If you don't know the format to which the string input must conform you can:
- Look for explanatory error messages
- Use random strings to explore the possibilities
- Use standard delimeters such as
, . ; : ' "
- Explore specifications to learn more about the application
- Explore source code or assembly code to learn how the input data is parsed and copied to various internal buffers
Data fuzzing is a powerful technique to automate the creation of attack strings. Data fuzzing relies on the modification of existing files or network packets in order to start from a known good format and produce input that is most likely to discover a buffer overrun. Some effective techniques which do not require the automation to be format aware are, randomly select a byte or bytes and:
- Add one to the value
- Subtract one from the value
- Replace with a bitwise XOR of the original value
- Insert additional bytes at this location
- Replace with a random byte
- Replace a random number of bytes starting at this location
- Insert a 0
- Replace with a 0
- Pick another random byte and transpose
3. Pass attack data to an entry point
It is possible to manually test for buffer overruns. However, due to the large number of variations in attack strings and the ease of automation it is preferrable to use automation that can pass your attack strings to the target interface. This is easier with programmatic methods such as an API or web service but is also possible with UI. A combination of automated attack generation and automated attack execution can allow your automation to find buffer overruns while you focus on other tasks.
4. Look for application crashes
When an application crashes due to a buffer overrun attack it is necessary to investigate further to determine if the crash is due to a buffer overrun.
To make it as easy as possible to investigate crashes, run your buffer overrun tests while the application is under a debugger. This allows you to more easily determine the reason for the crash, inspect the stack, inspect the heap and inspect your processor's registers for evidence of your attack string.
A common crash that occurs when applying a buffer overrun attack, which is not in fact a buffer overrun, is the null dereference. If you see your application has crashed due to a null dereference it is unlikely you have discovered a buffer overrun.
If you see pieces of your attack string on the heap you are likely to have discovered a heap-based buffer overrun.
If you see pieces of your attack string on the stack you are likely to have discovered a stack-based buffer overrun.
If you see pieces of your attack string in process registers you are very likely to have discovered a buffer overrun that is exploitable.
If you see pieces of your attack string in the EIP process register you have discovered a buffer overrun and it is exploitable.