diff --git a/.gitignore b/.gitignore index 96aa4d8..7680a37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc archived_fuzzies/*.session -.idea/ \ No newline at end of file +.idea/ +testing/ \ No newline at end of file diff --git a/AUTHORS.txt b/AUTHORS.txt index e1d0d1b..f50b4a0 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -6,5 +6,4 @@ Aaron Portnoy http://dvlabs.tippingpoint.com/team/aportnoy Ryan Sears - http://talesofacoldadmin.com http://fitblip.github.com/ \ No newline at end of file diff --git a/docs/generate_epydocs.bat b/docs/generate_epydocs.bat deleted file mode 100644 index fae8a18..0000000 --- a/docs/generate_epydocs.bat +++ /dev/null @@ -1 +0,0 @@ -c:\Python26\python.exe c:\Python26\Scripts\epydoc -o Sulley --css blue --name "Sulley: Fuzzing Framework" --url "http://pedram.openrce.org" ..\sulley \ No newline at end of file diff --git a/docs/img/crash_paths.gif b/docs/img/crash_paths.gif deleted file mode 100644 index bf73388..0000000 Binary files a/docs/img/crash_paths.gif and /dev/null differ diff --git a/docs/img/pcap.gif b/docs/img/pcap.gif deleted file mode 100644 index 40400d0..0000000 Binary files a/docs/img/pcap.gif and /dev/null differ diff --git a/docs/img/session_test.gif b/docs/img/session_test.gif deleted file mode 100644 index 4630155..0000000 Binary files a/docs/img/session_test.gif and /dev/null differ diff --git a/docs/img/sulley.jpg b/docs/img/sulley.jpg deleted file mode 100644 index 947d861..0000000 Binary files a/docs/img/sulley.jpg and /dev/null differ diff --git a/docs/img/sulley_web_interface.gif b/docs/img/sulley_web_interface.gif deleted file mode 100644 index 4d30d85..0000000 Binary files a/docs/img/sulley_web_interface.gif and /dev/null differ diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index a8110c0..0000000 --- a/docs/index.html +++ /dev/null @@ -1,894 +0,0 @@ - - - - - -
-
-

Sulley - Fuzzing Framework

- - -

Table of Contents

-
    -
  1. Overview
  2. - -
  3. Installation and Requirements
  4. -
  5. Data Representation
  6. -
  7. Session
  8. -
  9. Post Mortem
  10. -
  11. A Complete Walkthrough
  12. -
  13. Upcoming Features
  14. -
- - - -

Introduction

-Sulley is a fuzzer development and fuzz testing framework consisting of multiple extensible components. Sulley (IMHO) exceeds the capabilities of most previously published fuzzing technologies, commercial and public domain. The goal of the framework is to simplify not only data representation but to simplify data transmission and target monitoring as well. Sulley is affectionately named after the creature from Monsters Inc., because, well, he is fuzzy. -

-Modern day fuzzers are, for the most part, solely focus on data generation. Sulley not only has impressive data generation but has taken this a step further and includes many other important aspects a modern fuzzer should provide. Sulley watches the network and methodically maintains records. Sulley instruments and monitors the health of the target, capable of reverting to a good state using multiple methods. Sulley detects, tracks and categorizes detected faults. Sulley can fuzz in parallel, significantly increasing test speed. Sulley can automatically determine what unique sequence of test cases trigger faults. Sulley does all this, and more, automatically and without attendance. Overall usage of Sulley breaks down to the following: -
    -
  • Data Representation: First step in using any fuzzer. Run your target and tickle some interfaces while snagging the packets. Break down the protocol into indvidual requests and represent that as blocks in Sulley. -
  • Session: Link your developed requests together to form a session, attach the various available Sulley monitoring agents (socket, debugger, etc...) and commence fuzzing. -
  • Post Mortem: Review the generated data and monitored results. Replay individual test cases. -
- -

Sulley Directory Structure

-There is some rhyme and reason to the Sulley directory structure. Maintaining the directory structure will ensure that everything remains organized while you expand the fuzzer with Legos, requests and utilities. The following hierarchy outlines what you will need to know about the directory structure: -
    -
  • archived_fuzzies: This is a free form directory, organized by fuzz target name, to store archived fuzzers and data generated from fuzz sessions. -
      -
    • trend_server_protect_5168: This retired fuzz is referenced during the step by step walk through later in this document. -
    • trillian_jabber: Another retired fuzz referenced from the documentation. -
    -
  • audits: Recorded PCAPs, crash bins, code coverage and analysis graphs for active fuzz sessions should be saved to this directory. Once retired, recorded data should be moved to 'archived_fuzzies'. -
  • docs: This documentation and generated Epydoc API references. -
  • requests: Library of Sulley requests. Each target should get its own file which can be used to store multiple requests. -
      -
    • __REQUESTS__.html: This file contains the descriptions for stored request categories and lists individual types. Maintain alphabetical order. -
    • http.py: Various web server fuzzing requests. -
    • trend.py: Contains the requests associated with the complete fuzz walkthrough discussed later in this document. -
    -
  • sulley: The fuzzer framework. Unless you want to extend the framework, you shouldn't need to touch these files. -
      -
    • legos: User-defined complex primitives. -
        -
      • ber.py: ASN.1 / BER primitives. -
      • dcerpc.py: Microsoft RPC NDR primitives. -
      • misc.py: Various uncategorized complex primitives such as e-mail addresses and hostnames. -
      • xdr.py: XDR types -
      -
    • pgraph: Python graph abstraction library. Utilized in building sessions. -
    • utils: Various helper routines. -
        -
      • dcerpc.py: Microsoft RPC helper routines such as for binding to an interface and generating a request. -
      • misc.py: Various uncategorized routines such as CRC-16 and UUID manipulation routines. -
      • scada.py: SCADA specific helper routines including a DNP3 block encoder. -
      -
    • __init__.py: The various 's_' aliases that are used in creating requests are defined here. -
    • blocks.py: Blocks and block helpers are defined here. -
    • instrumentation.py: External instrumentation feature to monitor targets not supporting debugger. -
    • pedrpc.py: This file defines client and server classes which are used by Sulley for communications between the various agents and the main fuzzer. -
    • primitives.py: The various fuzzer primitives including static, random, strings and integers are defined here. -
    • sessions.py: Functionality for building and executing a session. -
    • sex.py: Sulley's custom exception handling class. -
    -
  • unit_tests: Sulley's unit testing harness. -
  • utils: Various stand-alone utilities. -
      -
    • crashbin_explorer.py: Command line utility for exploring the results stored in serialized crash bin files. -
    • pcap_cleaner.py: Command line utility for cleaning out a PCAP directory of all entries not associated with a fault. -
    -
  • network_monitor.py: PedRPC driven network monitoring agent. -
  • process_monitor.py: PedRPC driven debugger-based target monitoring agent. -
  • unit_test.py: Sulley's unit testing harness. -
  • vmcontrol.py: PedRPC driven VMWare controlling agent. -
- - - -

Authors

-Pedram Amini
-http://pedram.openrce.org -

-Aaron Portnoy
-http://dvlabs.tippingpoint.com/team/aportnoy -

-Sulley is additionally maintained and built upon by members of the TippingPoint Security Research Team. - - - -

Installation and Requirements

-For simple data representation and transmission there are no requirements. However, the various monitoring components do have further needs: - - - - -

Data Representation

-Aitel had it right with SPIKE, we've taken a good look at every fuzzer I can get my hands on and the block based approach to protocol representation stands above the others combining both simplicity and the flexibility to represent most protocols. Sulley utilizes a block based approach to generate individual "requests". These requests are then later tied together to form a "session". To begin, initialize with a new name for your request: -
-s_initialize("new request")
-
-Now you start adding primitives, blocks and nested blocks to the request. Each primitive can be individually rendered and mutated. Rendering a primitive returns its contents in raw data format. Mutating a primitive transforms its internal contents. The concepts of rendering and mutating are abstracted from fuzzer developers for the most part, so don't worry about it. Know however that each mutatable primitive accepts a default value which is restored when the fuzzable values are exhausted. - -

Static and Random Primitives

-Let's begin with the simplest primitive, s_static(), which adds a static unmutating value of arbitrary length to the request. There are various aliases sprinkled throughout Sulley for your convenience, s_dunno(), s_raw() and s_unknown() are aliases of s_static(): -
-# these are all equivalent:
-s_static("pedram\x00was\x01here\x02")
-s_raw("pedram\x00was\x01here\x02")
-s_dunno("pedram\x00was\x01here\x02")
-s_unknown("pedram\x00was\x01here\x02")
-
-Primitives, blocks etc. all take an optional name keyword argument. Specifying a name allows you to access the named item directly from the request via request.names["name"] instead of having to walk the block structure to reach the desired element. -

-Related to the above, but not equivalent, is the s_binary() primitive which accepts binary data represented in multiple formats. SPIKE users will recognize this API, its functionality is (or rather should be) equivalent to what you are already familiar with: -
-    # yeah, it can handle all these formats.
-    s_binary("0xde 0xad be ef \xca fe 00 01 02 0xba0xdd f0 0d", name="complex")
-
-Most of Sulley's primitives are driven by "fuzz heuristics" and therefore have a limited number of mutations. An exception to this is the s_random() primitive which can be utilized to generate random data of varying lengths. This primitive takes two mandatory arguments, 'min_length' and 'max_length', specifying the minimum and maximum length of random data to generate on each iteration respectively. This primitive also accepts the following optional keyword arguments: -
    -
  • num_mutations: (integer, default=25) Number of mutations to make before reverting to default. -
  • fuzzable: (boolean, default=True) Enable or disable fuzzing of this primitive. -
  • name: (string, default=None) As with all Sulley objects specifying a name gives you direct access to this primitive throughout the request. -
-The 'num_mutations' keyword argument specifies how many times this primitive should be re-rendered before it is considered exhausted. To fill a static sized field with random data, set the values for 'min_length' and 'max_length' to be the same. - -

Integers

-Binary and ASCII protocols alike have various sized integers sprinkled all throughout them, for instance the Content-Length field in HTTP. Like most fuzzing frameworks, a portion of Sulley is dedicated to representing these types: -
    -
  • 1 byte: s_byte(), s_char() -
  • 2 bytes: s_word(), s_short() -
  • 4 bytes: s_dword(), s_long(), s_int() -
  • 8 bytes: s_qword(), s_double() -
-The integer types each accept at least a single parameter, the default integer value. Additionally the following optional keyword arguments may be specified: -
    -
  • endian: (character, default='<') Endianess of the bit field. Specify '<' for little endian and '>' for big endian. -
  • format: (string, default="binary") Output format, "binary" or "ascii", controls the format which the integer primitives renders in. For example the value 100 is rendered as "100" in ASCII and "\x64" in binary. -
  • signed: (boolean, default=False) Make size signed vs. unsigned, applicable only when format="ascii". -
  • full_range: (boolean, default=False) If enabled then this primitive mutates through all possible values. More on this later. -
  • fuzzable: (boolean, default=True) Enable or disable fuzzing of this primitive. -
  • name: (string, default=None) As with all Sulley objects specifying a name gives you direct access to this primitive throughout the request. -
-The full_range modifier is of particular interest from above. Consider you want to fuzz a DWORD value, that's 4,294,967,295 total possible values. At a rate of 10 test cases per second, it would take 13 years to finish fuzzing this single primitive! To reduce this vast input space Sulley defaults to trying only "smart" values. This includes the plus and minus 10 border cases around 0, the maximum integer value (MAX_VAL), MAX_VAL divided by 2, MAX_VAL divided by 3, MAX_VAL divided by 4, MAX_VAL divided by 8, MAX_VAL divided by 16 and MAX_VAL divided by 32. Exhausting this reduced input space of 141 test cases requires only seconds. - -

Strings and Delimiters

-Strings can be found everywhere. E-mail addresses, hostnames, usernames, passwords and more all examples of string components you will no doubt come across when fuzzing. Sulley provides the s_string() primitive for representing these fields. The primitive takes a single mandatory argument specifying the default, valid, value for the primitive. The following additional keyword arguments may be specified: -
    -
  • size: (integer, default=-1) Static size for this string. For dynamic sizing, leave this as -1. -
  • padding: (character, default='\x00') If an explicit size is specified and the generated string is smaller than size, use this value to padd the field up to size. -
  • encoding: (string, default="ascii") Encoding to use for string. Valid options include whatever the Python str.encode() routine can accept. For Microsoft Unicode strings, specify "utf_16_le". -
  • fuzzable: (boolean, default=True) Enable or disable fuzzing of this primitive. -
  • name: (string, default=None) As with all Sulley objects specifying a name gives you direct access to this primitive throughout the request. -
-Strings are frequently parsed into sub-fields through the usage of delimiters. The space character for example is used as a delimiter in the HTTP request "GET /index.html HTTP/1.0". The front slash (/) and dot (.) characters in that same request are also delimiters. When defining a protocol in Sulley, be sure to represent delimiters using the s_delim() primitive. As with other primitives, the first argument is mandatory and used to specify the default value. Also as with other primitives, s_delim() accepts the optionals 'fuzzable' and 'name' keyword arguments. Delimiter mutations include repetition, substitution and exclusion. As a complete example, consider the following sequence of primitives for fuzzing the HTML body tag. -
-# fuzzes the string: <BODY bgcolor="black">
-s_delim("<")
-s_string("BODY")
-s_delim(" ")
-s_string("bgcolor")
-s_delim("=")
-s_delim("\"")
-s_string("black")
-s_delim("\"")
-s_delim(">")
-
- -

Blocks

-Having mastered primitives, let's next take a look at how they may be organized and nested within blocks. New blocks are defined and opened with s_block_start() and closed with s_block_end(). Each block must be given a name, specified as the first argument to s_block_start(). This routine also accepts the following optional keyword arguments: -
    -
  • group: (string, default=None) Name of group to associate this block with, more on this later. -
  • encoder: (function pointer, default=None) Pointer to a function to pass rendered data to prior to returning it. -
  • dep: (string, default=None) Optional primitive whose specific value this block is dependant on. -
  • dep_value: (mixed, default=None) Value that field "dep" must contain for block to be rendered. -
  • dep_values: (list of mixed types, default=[]) Values that field "dep" may contain for block to be rendered. -
  • dep_compare (string, default="==") Comparison method to apply to dependency. Valid options include: "==", "!=", ">", ">=", "<" and "<=". -
-Grouping, encoding and dependencies are powerful features not seen in most other frameworks and deserve further dissection. - -

Groups

-Grouping allows you to tie a block to a group primitive to specify that the block should cycle through all possible mutations for each value within the group. The group primitive is useful for example for representing a list of valid opcodes or verbs with similar argument structures. The primitive s_group() defines a group and accepts two mandatory arguments. The first specifies the name of the group, the second specifies the list of possible raw values to iterate through. As a simple example, consider the following complete Sulley request designed to fuzz a web server: -
-# import all of Sulley's functionality.
-from sulley import *
-
-# this request is for fuzzing: {GET,HEAD,POST,TRACE} /index.html HTTP/1.1
-
-# define a new block named "HTTP BASIC".
-s_initialize("HTTP BASIC")
-
-# define a group primitive listing the various HTTP verbs we wish to fuzz.
-s_group("verbs", values=["GET", "HEAD", "POST", "TRACE"])
-
-# define a new block named "body" and associate with the above group.
-if s_block_start("body", group="verbs"):
-    # break the remainder of the HTTP request into individual primitives.
-    s_delim(" ")
-    s_delim("/")
-    s_string("index.html")
-    s_delim(" ")
-    s_string("HTTP")
-    s_delim("/")
-    s_string("1")
-    s_delim(".")
-    s_string("1")
-    # end the request with the mandatory static sequence.
-    s_static("\r\n\r\n")
-# close the open block, the name argument is optional here.
-s_block_end("body")
-
-The script begins by importing all of Sulley's components. Next a new request is initialized and given the name "HTTP BASIC". This name can later be referenced for accessing this request directly. Next, a group is defined with the name "verbs" and the possible string values "GET", "HEAD", "POST" and "TRACE". A new block is started with the name "body" and tied to the previously defined group primitive through the optional 'group' keyword argument. Note that s_block_start() always returns True which allows you to optionally "tab out" its contained primitives using a simple if-clause. Also note that the name argument to s_block_end() is optional. These framework design decisions were made purely for aesthetic purposes. A series of basic delimiter and string primitives are then defined within the confinements of the "body" block and the block is closed. When this defined request is loaded into a Sulley session, the fuzzer will generate and transmit all possible values for the block "body", once for each verb defined in the group. - -

Encoders

-Encoders are a simple, yet powerful block modifier. A function can be specified and attached to a block in order to modify the rendered contents of that block prior to return and transmission over the wire. This is best explained with a real world example. The DcsProcessor.exe daemon from Trend Micro Control Manager listens on TCP port 20901 and expects to receive data formatted with a proprietary XOR encoding routine. Through reverse engineering of the decoder, the following XOR encoding routine was developed: -
-def trend_xor_encode (str):
-    key = 0xA8534344
-    ret = ""
-
-    # pad to 4 byte boundary.
-    pad = 4 - (len(str) % 4)
-
-    if pad == 4:
-        pad = 0
-
-    str += "\x00" * pad
-
-    while str:
-        dword  = struct.unpack("<L", str[:4])[0]
-        str    = str[4:]
-        dword ^= key
-        ret   += struct.pack("<L", dword)
-        key    = dword
-
-    return ret
-
-Sulley encoders take a single parameter, the data to encode and return the encoded data. This defined encoder can now be attached to a block containing fuzzable primitives allowing the fuzzer developer to continue as if this little hurdle never existed. - -

Dependencies

-Dependencies allow you to apply a conditional to the rendering of an entire block. This is accomplished by first linking a block to a primitive it will be dependant on using the optional 'dep' keyword parameter. When the time comes for Sulley to render the dependant block, it will check the value of the linked primitive and behave accordingly. A dependant value can be specified with the 'dep_value' keyword parameter. Alternatively a list of dependant values can be specified with the 'dep_values' keyword parameter. Finally, the actual conditional comparison can be modified through the 'dep_compare' keyword parameter. For example, consider a situation where depending on the value of an integer, different data is expected: -
-s_short("opcode", full_range=True)
-
-# opcode 10 expects an authentication sequence.
-if s_block_start("auth", dep="opcode", dep_value=10):
-    s_string("USER")
-    s_delim(" ")
-    s_string("pedram")
-    s_static("\r\n")
-    s_string("PASS")
-    s_delim(" ")
-    s_delim("fuzzywuzzy")
-s_block_end()
-
-# opcodes 15 and 16 expect a single string hostname.
-if s_block_start("hostname", dep="opcode", dep_values=[15, 16]):
-    s_string("pedram.openrce.org")
-s_block_end()
-
-# the rest of the opcodes take a string prefixed with two underscores.
-if s_block_start("something", dep="opcode", dep_values=[10, 15, 16], dep_compare="!="):
-    s_static("__")
-    s_string("some string")
-s_block_end()
-
-Block dependencies can be chained together in any number of ways allowing for powerful (and unfortunately complex) combinations. - -

Block Helpers

-An important aspect of data generation that you must become familiar with to effectively utilize Sulley are block helpers. This includes sizers, checksums and repeaters. - -

Sizers

-SPIKE users will be familiar with the s_sizer() (or s_size()) block helper. This helper takes the block name to measure the size of as the first parameter and accepts the following additional keyword arguments: -
    -
  • length: (integer, default=4) Length of size field. -
  • endian: (character, default='<') Endianess of the bit field. Specify '<' for little endian and '>' for big endian. -
  • format: (string, default="binary") Output format, "binary" or "ascii", controls the format which the integer primitives renders in. -
  • inclusive: (boolean, default=False) Should the sizer count its own length? -
  • signed: (boolean, default=False) Make size signed vs. unsigned, applicable only when format="ascii". -
  • fuzzable: (boolean, default=False) Enable or disable fuzzing of this primitive. -
  • name: (string, default=None) As with all Sulley objects specifying a name gives you direct access to this primitive throughout the request. -
-Sizers are a crucial component in data generation that allow for the representation of complex protocols such as XDR notation, ASN.1 etc. Sulley will dynamically calculate the length of the associated block when rendering the sizer. By default Sulley will not fuzz size fields. In many cases this is the desired behaviour, in the event it isn't however, enable the 'fuzzable' flag. - -

Checksums

-Similar to sizers, the s_checksum() helper takes the block name to calculate the checksum of as the first parameter. The following optional keyword arguments may also be specified: -
    -
  • algorithm: (string or function pointer, default="crc32"). Checksum algorithm to apply to target block. (crc32, adler32, md5, sha1) -
  • endian: (character, default='<') Endianess of the bit field. Specify '<' for little endian and '>' for big endian. -
  • length: (integer, default=0) Length of checksum, leave as 0 to auto-calculate. -
  • name: (string, default=None) As with all Sulley objects specifying a name gives you direct access to this primitive throughout the request. -
-The 'algorithm' argument can be one of "crc32", "adler32", "md5" or "sha1". Alternatively, you can specify a function pointer for this parameter to apply a custom checksum algorithm. - -

Repeaters

-The s_repeat() (or s_repeater()) helper is used for replicating a block a variable number of times. This is useful for example when testing for overflows during the parsing of tables with multiple elements. This helper takes three mandatory arguments, the name of the block to be repeated, the minimum number of repetitions and the maximum number of repetitions. Additionally, the following optional keyword arguments are available: -
    -
  • step: (integer, default=1) Step count between min and max reps. -
  • fuzzable: (boolean, default=False) Enable or disable fuzzing of this primitive. -
  • name: (string, default=None) As with all Sulley objects specifying a name gives you direct access to this primitive throughout the request. -
-Consider the following example that ties all three of the introduced helpers together. We are fuzzing a portion of a protocol which contains a table of strings. Each entry in the table consists of a 2-byte string type field, a 2-byte length field, a string field and finally a CRC-32 checksum field which is calculated over the string field. We don't know what the valid values for the type field are, so we'll fuzz that with random data. Here is what this portion of the protocol may look like in Sulley: -
-# table entry: [type][len][string][checksum]
-if s_block_start("table entry"):
-    # we don't know what the valid types are, so we'll fill this in with random data.
-    s_random("\x00\x00", 2, 2)
-    
-    # next, we insert a sizer of length 2 for the string field to follow.
-    s_size("string field", length=2)
-    
-    # block helpers only apply to blocks, so encapsulate the string primitive in one.
-    if s_block_start("string field"):
-        # the default string will simply be a short sequence of C's.
-        s_string("C" * 10)
-    s_block_end()
-    
-    # append the CRC-32 checksum of the string to the table entry.
-    s_checksum("string field")
-s_block_end()
-
-# repeat the table entry from 100 to 1,000 reps stepping 50 elements on each iteration.
-s_repeat("table entry", min_reps=100, max_reps=1000, step=50)
-
-This Sulley script will fuzz not only table entry parsing but may potentially discover a fault in the processing of overly long tables. - -

Legos

-Sulley utilizes "Legos" for representing user-defined components such as e-mail addresses, hostnames and protocol primitives used in Microsoft RPC, XDR, ASN.1 and others. In ASN.1 / BER strings are represented as the sequence [0x04][0x84][dword length][string]. When fuzzing an ASN.1 based protocol, including the length and type prefixes in front of every string can become cumbersome. Instead we can define a Lego and reference it: -
-s_lego("ber_string", "anonymous")
-
-Every Lego follows a similar format with the exception of the optional 'options' keyword argument, which is specific to individual legos. As a simple example, consider the definition of the 'tag' lego, helpful when fuzzing XML-ish protocols: -
-class tag (blocks.block):
-    def __init__ (self, name, request, value, options={}):
-        blocks.block.__init__(self, name, request, None, None, None, None)
-
-        self.value   = value
-        self.options = options
-
-        if not self.value:
-            raise sex.SullyRuntimeError("MISSING LEGO.tag DEFAULT VALUE")
-
-        # 
-        # [delim][string][delim]
-
-        self.push(primitives.delim("<"))
-        self.push(primitives.string(self.value))
-        self.push(primitives.delim(">"))
-
-This example Lego simply accepts the desired tag as a string and encapsulates it within the appropriate delimiters. It does so by extending the block class and manually adding the tag delimiters and user-supplied string to the block stack via self.push(). -

-Here is another example which produces a simple lego for representing ASN.1 / BER integers in Sulley. The "lowest common denominator" was chosen to represent all integers as 4-byte integers which following the form: [0x02][0x04][dword], where 0x02 specifies integer type, 0x04 specifies the integer is 4 bytes long and the 'dword' represents the actual integer we are passing. Here is what the definition looks like from sulley\legos\ber.py: -
-class integer (blocks.block):
-    def __init__ (self, name, request, value, options={}):
-        blocks.block.__init__(self, name, request, None, None, None, None)
-
-        self.value   = value
-        self.options = options
-
-        if not self.value:
-            raise sex.SullyRuntimeError("MISSING LEGO.ber_integer DEFAULT VALUE")
-
-        self.push(primitives.dword(self.value, endian=">"))
-
-
-    def render (self):
-        # let the parent do the initial render.
-        blocks.block.render(self)
-
-        self.rendered = "\x02\x04" + self.rendered
-        return self.rendered
-
-Similar to the previous example, the supplied integer is added to the block stack with self.push(). Unlike the previous example, the render() routine is overloaded to prefix the rendered contents with the static sequence "\x02\x04" to satisfy the integer representation requirements previously described. - -

Final Notes

-Sulley grows with the creation of every new fuzzer. Developed blocks/requests expand the request library and can be easily referenced and used in the construction of future fuzzers. For a more detailed API reference, see the Epydoc generated Sulley API Docs. - - -

Session

-Once you have defined a number of requests it's time to tie them together in a session. One of the major benefits of Sulley over other fuzzing frameworks is its capability of fuzzing "deep" within a protocol. This is accomplished by linking requests together in a graph. In the following example a sequence of requests are tied together and the pgraph library, which the session and request classes extend from, is leveraged to render the graph in uDraw format: - -
-
-from sulley import *
-
-s_initialize("helo")
-s_static("helo")
-
-s_initialize("ehlo")
-s_static("ehlo")
-
-s_initialize("mail from")
-s_static("mail from")
-
-s_initialize("rcpt to")
-s_static("rcpt to")
-
-s_initialize("data")
-s_static("data")
-
-sess = sessions.session()
-sess.connect(s_get("helo"))
-sess.connect(s_get("ehlo"))
-sess.connect(s_get("helo"),      s_get("mail from"))
-sess.connect(s_get("ehlo"),      s_get("mail from"))
-sess.connect(s_get("mail from"), s_get("rcpt to"))
-sess.connect(s_get("rcpt to"),   s_get("data"))
-
-fh = open("session_test.udg", "w+")
-fh.write(sess.render_graph_udraw())
-fh.close()
-
-
-When it comes time to fuzz, Sulley walks the graph structure starting with the root node and fuzzing each component along the way. In this example it will begin with the 'helo' request. Once complete, Sulley will begin fuzzing the 'mail from' request. It does so by prefixing each test case with a valid 'helo' request. Next, Sulley moves on to fuzzing the 'rcpt to' request. Again, this is accomplished by prefixing each test case with a valid 'helo' and 'mail from' request. The process continues through 'data' and then restarts down the 'ehlo' path. The ability to break a protocol into individual requests and fuzz all possible paths through the constructed protocol graph is powerful. Consider for example an issue disclosed against Ipswitch Collaboration Suite in September of 2006. The software fault in this case was a stack overflow during the parsing of long strings contained within the characters '@' and ':'. What makes this case interesting is that this vulnerability is only exposed over the 'ehlo' route and not the 'helo' route. If our fuzzer is unable to walk all possible protocol paths, then issues such as this may be missed. -

-When instantiating a session, the following optional keywords arguments may be specified: -
    -
  • session_filename: (string, default=None) Filename to serialize persistent data to. Specifying a filename allows you to stop and resume the fuzzer. -
  • skip: (integer, default=0) Number of test cases to skip. -
  • sleep_time: (float, default=1.0) Time to sleep in between transmission of test cases. -
  • log_level: (integer, default=2) Set the log level, higher number == more log messages. -
  • proto: (string, default="tcp") Communication protocol. -
  • timeout: (float, default=5.0) Seconds to wait for a send() / recv() to return prior to timing out. -
-Another advanced feature that Sulley introduces is the ability to register callbacks on every edge defined within the protocol graph structure. This allows us to register a function to call between node transmissions to implement functionality such as challenge response systems. The callback method must follow this prototype: -
-    def callback(node, edge, last_recv, sock)
-
-Where 'node' is the node about to be sent, 'edge' is the last edge along the current fuzz path to 'node', 'last_recv' contains the data returned from the last socket transmission and 'sock' is the live socket. A callback is also useful in situations where, for example, the size of the next pack is specified in the first packet. As another example, if you need to fill in the dynamic IP address of the target register a callback that snags the IP from sock.getpeername()[0]. Edge callbacks can also be registered through the optional keyword argument 'callback' to the session.connect() method. - -

Targets and Agents

-The next step is to define targets, link them with agents and add the targets to the session. In the following example we instantiate a new target which is running inside a VMWare virtual machine and link it to three agents: -
-target = sessions.target("10.0.0.1", 5168)
-
-target.netmon    = pedrpc.client("10.0.0.1",  26001)
-target.procmon   = pedrpc.client("10.0.0.1",  26002)
-target.vmcontrol = pedrpc.client("127.0.0.1", 26003)
-
-target.procmon_options = \
-{
-    "proc_name"      : "SpntSvc.exe",
-    "stop_commands"  : ['net stop "trend serverprotect"'],
-    "start_commands" : ['net start "trend serverprotect"'],
-}
-
-sess.add_target(target)
-sess.fuzz()
-
-The instantiated target is bound on TCP port 5168 on the host 10.0.0.1. A network monitor agent is running on the target system, listening by default on port 26001. The network monitor will record all socket communications to individual PCAP files labeled by test case number. The process monitor agent is also running on the target system, listening by default on port 26002. This agent accepts additional arguments specifying the process name to attach to, the command to stop the target process and the command to start the target process. Finally the VMWare control agent is running on the local system, listening by default on port 26003. The target is added to the session and fuzzing begins. Sulley is capable of fuzzing multiple targets, each with a unique set of linked agents. This allows you to save time by splitting the total test space across the various targets. -

-Let's take a closer look at each individual agents functionality. - -

Agent: Network Monitor (network_monitor.py)

-The network monitor agent is responsible for monitoring network communications and logging them to PCAP files on disk. The agent is hard coded to bind to TCP port 26001 and accepts connections from the Sulley session over the PedRPC custom binary protocol. Prior to transmitting a test case to the target, Sulley contacts this agent and requests that it begins recording network traffic. Once the test case has been successfully transmitted, Sulley again contacts this agent requesting it to flush recorded traffic to a PCAP file on disk. The PCAP files are named by test case number for easy retrieval. This agent does not have to be launched on the same system as the target software. It must however have visibility into sent and received network traffic. This agent accepts the following command line arguments: -
-ERR> USAGE: network_monitor.py
-    <-d|--device DEVICE #>    device to sniff on (see list below)
-    [-f|--filter PCAP FILTER] BPF filter string
-    [-p|--log_path PATH]      log directory to store pcaps to
-    [-l|--log_level LEVEL]    log level (default 1), increase for more verbosity
-
-Network Device List:
-    [0] \Device\NPF_GenericDialupAdapter
-    [1] {2D938150-427D-445F-93D6-A913B4EA20C0}  192.168.181.1
-    [2] {9AF9AAEC-C362-4642-9A3F-0768CDA60942}  0.0.0.0
-    [3] {9ADCDA98-A452-4956-9408-0968ACC1F482}  192.168.81.193
-    ...
-
- -

Agent: Process Monitor (process_monitor.py)

-The process monitor agent is responsible for detecting faults which may occur in the target process during fuzz testing. The agent is hard coded to bind to TCP port 26002 and accepts connections from the Sulley session over the PedRPC custom binary protocol. After have successfully transmitted each individual test case to the target, Sulley contacts this agent to determine if a fault was triggered. If so, high level information regarding the nature of the fault is transmitted back to the Sulley session for display through the internal web server (more on this later). Triggered faults are also logged in a serialized "crash bin" for post mortem analysis. This functionality is explored in further detailed later. This agent accepts the following command line arguments: -
-ERR> USAGE: process_monitor.py
-    <-c|--crash_bin FILENAME> filename to serialize crash bin class to
-    [-p|--proc_name NAME]     process name to search for and attach to
-    [-i|--ignore_pid PID]     ignore this PID when searching for the target process
-    [-l|--log_level LEVEL]    log level (default 1), increase for more verbosity
-
- -

Agent: VMWare Control (vmcontrol.py)

-The VMWare control agent is hard coded to bind to TCP port 26003 and accepts connections from the Sulley session over the PedRPC custom binary protocol. This agent exposes an API for interacting with a virtual machine image including the ability to start, stop, suspend or reset the image as well as take, delete and restore snapshots. In the event that a fault has been detected or the target can not be reached, Sulley can contact this agent and revert the virtual machine to a known good state. The test sequence honing tool will heavily rely on this agent to accomplish its task of identifying the exact sequence of test cases that trigger any given complex fault. This agent accepts the following command line arguments: -
-ERR> USAGE: vmcontrol.py
-    <-x|--vmx FILENAME>    path to VMX to control
-    <-r|--vmrun FILENAME>  path to vmrun.exe
-    [-s|--snapshot NAME>   set the snapshot name
-    [-l|--log_level LEVEL] log level (default 1), increase for more verbosity
-
- -

External instrumentation

-Some kind of targets (embedded devices for example) don't support debugger, and the process monitor agent can't be used in these cases. The external instrumentation class allows external commands to be called to detect fault and restart the target . SSH is used in the following example, but any python function or external script can be used: -
-import os
-
-def ssh_is_alive():
-    '''Check that the target is alive. Called after each test case. Return True if alive, False otherwise'''
-
-    _, stdout = os.popen2('ssh %s pidof target' % IP_DST)
-    pid = stdout.read()
-    return pid != ''
-
-def ssh_restart():
-    '''Restart the target. Called when instrumentation (post) fail.'''
-
-    os.popen2('ssh %s /etc/init.d/target restart' % IP_DST)
-
-sess           = sessions.session()
-target         = sessions.target(IP_DST, PORT_DST)
-target.procmon = instrumentation.external(post=ssh_is_alive, start=ssh_restart)
-sess.add_target(target)
-sess.connect(s_get('node'))
-sess.fuzz()
-
- -

Web Monitoring Interface

-The Sulley session class has a built in minimal web server which is hard coded to bind to port 26000. Once the fuzz() method of the session class is called the web server thread spins off and the progress of the fuzzer including intermediary results can be seen. Here is an example screenshot: -



-The fuzzer can be paused and resumed by hitting the appropriate buttons. A synopsis of each detected fault is displayed as a list with the offending test case number listed in the first column. Clicking on the test case number loads a detailed crash dump at the time of the fault. This information is of course also available in the "crash bin" file and accessible programmatically. - - -

Post Mortem

-Once a Sulley fuzz session is complete, it is time to review the results and enter the post mortem phase. The sessions built in web server will provide you with early indications on potentially uncovered issues, but this is the time you will actually separate out the results. A couple of utilities exist to help you along in this process. The first is the 'crashbin_explorer.py' utility, which accepts the following command line arguments: -
-$ ./utils/crashbin_explorer.py
-    USAGE: crashbin_explorer.py <xxx.crashbin>
-        [-t|--test #]     dump the crash synopsis for a specific test case number
-        [-g|--graph name] generate a graph of all crash paths, save to 'name'.udg
-
-We can use this utility for example to view every location where a fault was detected at and furthermore list the individual test case numbers which triggered a fault at that address. The following results are from a real world audit against Trillians Jabber protocol parser: -
-$ ./utils/crashbin_explorer.py audits/trillian_jabber.crashbin
-    [3] ntdll.dll:7c910f29 mov ecx,[ecx] from thread 664 caused access violation
-            1415, 1416, 1417,
-    [2] ntdll.dll:7c910e03 mov [edx],eax from thread 664 caused access violation
-            3780, 9215,
-    [24] rendezvous.dll:4900c4f1 rep movsd from thread 664 caused access violation
-            1418, 1419, 1420, 1421, 1422, 1423, 1424, 1425, 3443, 3781, 3782, 3783, 3784, 3785, 3786, 3787, 9216, 9217, 9218, 9219, 9220, 9221, 9222, 9223,
-    [1] ntdll.dll:7c911639 mov cl,[eax+0x5] from thread 664 caused access violation
-            3442,
-
-None of these listed fault points may stand out as an obviously exploitable issue. We can further drill down into the specifics of an individual fault by specifying a test case number with the '-t' command line switch. Let's take a look at test case number 1416: -
-$ ./utils/crashbin_explorer.py audits/trillian_jabber.crashbin -t 1416
-    ntdll.dll:7c910f29 mov ecx,[ecx] from thread 664 caused access violation
-    when attempting to read from 0x263b7467
-    CONTEXT DUMP
-      EIP: 7c910f29 mov ecx,[ecx]
-      EAX: 039a0318 (  60424984) -> gt;&gt;&gt;...&gt;&gt;&gt;&gt;&gt;(heap)
-      EBX: 02f40000 (  49545216) ->    PP@ (heap)
-      ECX: 263b7467 ( 641430631) -> N/A
-      EDX: 263b7467 ( 641430631) -> N/A
-      EDI: 0399fed0 (  60423888) -> #e<root><message>&gt;&gt;&gt;&gt;...&gt;&gt;&gt;& (heap)
-      ESI: 039a0310 (  60424976) -> gt;&gt;&gt;...&gt;&gt;&gt;&gt;&gt;(heap)
-      EBP: 03989c38 (  60333112) -> \|gt;&t]IP"Ix;IXIox@ @x@PP8|p|Hg9I P (stack)
-      ESP: 03989c2c (  60333100) -> \|gt;&t]IP"Ix;IXIox@ @x@PP8|p|Hg9I (stack)
-      +00: 02f40000 (  49545216) ->    PP@ (heap)
-      +04: 0399fed0 (  60423888) -> #e<root><message>&gt;&gt;&gt;&gt;...&gt;&gt;&gt;& (heap)
-      +08: 00000000 (         0) -> N/A
-      +0c: 03989d0c (  60333324) -> Hg9I Pt]I@"ImI,IIpHsoIPnIX{ (stack)
-      +10: 7c910d5c (2089880924) -> N/A
-      +14: 02f40000 (  49545216) ->    PP@ (heap)
-    disasm around:
-            0x7c910f18 jnz 0x7c910fb0
-            0x7c910f1e mov ecx,[esi+0xc]
-            0x7c910f21 lea eax,[esi+0x8]
-            0x7c910f24 mov edx,[eax]
-            0x7c910f26 mov [ebp+0xc],ecx
-            0x7c910f29 mov ecx,[ecx]
-            0x7c910f2b cmp ecx,[edx+0x4]
-            0x7c910f2e mov [ebp+0x14],edx
-            0x7c910f31 jnz 0x7c911f21
-    
-    stack unwind:
-            ntdll.dll:7c910d5c
-            rendezvous.dll:49023967
-            rendezvous.dll:4900c56d
-            kernel32.dll:7c80b50b
-    SEH unwind:
-            03989d38 -> ntdll.dll:7c90ee18
-            0398ffdc -> rendezvous.dll:49025d74
-            ffffffff -> kernel32.dll:7c8399f3
-
-Again nothing too obvious may stand out but we know that we are influencing this specific access violation as the register being invalidly dereferenced, ECX, contains the ASCII string: "&;tg". String expansion issue perhaps? We can view the crash locations graphically which adds an extra dimension displaying the known execution paths using the '-g' command line switch. The following generated graph is again from a real world audit against the Trillian Jabber parser: -



-We can see that although we've uncovered 4 different crash locations, the source of the issue appears to be the same. Further research reveals that this is indeed correct. The specific flaw exists in the Rendezvous / XMPP (Extensible Messaging and Presence Protocol) messaging subsystem. Trillian locates nearby users through the '_presence' mDNS (multicast DNS) service on UDP port 5353. Once a user is registered through mDNS, messaging is accomplished via XMPP over TCP port 5298. Within plugins\rendezvous.dll the follow logic is applied to received messages: -
-    4900C470 str_len:
-    4900C470     mov cl, [eax]      ; *eax = message+1
-    4900C472     inc eax
-    4900C473     test cl, cl
-    4900C475     jnz short str_len
-
-    4900C477     sub eax, edx
-    4900C479     add eax, 128       ; strlen(message+1) + 128
-    4900C47E     push eax
-    4900C47F     call _malloc
-
-The string length of the the supplied message is calculated and a heap buffer in the amount of length + 128 is allocated to store a copy of the message which is then passed through expatxml.xmlComposeString(), a function called with the following prototype: -
-    plugin_send(MYGUID, "xmlComposeString", struct xml_string_t *);
-
-    struct xml_string_t {
-        unsigned int      struct_size;
-        char              *string_buffer;
-        struct xml_tree_t *xml_tree;
-    };
-
-The xmlComposeString() routine calls through to expatxml.19002420() which, among other things, HTML encodes the characters &, > and < as &, > and < respectively. This behavior can be seen in the following disassembly snippet: -
-    19002492 push 0
-    19002494 push 0
-    19002496 push offset str_Amp       ; "&"
-    1900249B push offset ampersand     ; "&"
-    190024A0 push eax
-    190024A1 call sub_190023A0
-
-    190024A6 push 0
-    190024A8 push 0
-    190024AA push offset str_Lt        ; "<"
-    190024AF push offset less_than     ; "<"
-    190024B4 push eax
-    190024B5 call sub_190023A0
-
-    190024BA push
-    190024BC push
-    190024BE push offset str_Gt        ; ">"
-    190024C3 push offset greater_than  ; ">"
-    190024C8 push eax
-    190024C9 call sub_190023A0
-
-As the originally calculated string length does not account for this string expansion, the following subsequent in-line memory copy operation within rendezvous.dll can trigger an exploitable memory corruption: -
-    4900C4EC mov ecx, eax
-    4900C4EE shr ecx, 2
-    4900C4F1 rep movsd
-    4900C4F3 mov ecx, eax
-    4900C4F5 and ecx, 3
-    4900C4F8 rep movsb
-
-Each of the faults detected by Sulley were in response to this logic error. Tracking fault locations and paths allowed us to quickly postulate that a single source was responsible. A final step we may wish to take is to remove all PCAP files which do not contain information regarding a fault. The 'pcap_cleaner.py' utility was written for exactly this task: -
-$ ./utils/pcap_cleaner.py
-    USAGE: pcap_cleaner.py <xxx.crashbin> <path to pcaps>
-
-This utility will open the specified crashbin file, read in the list of test cases numbers that triggered a fault and erase all other PCAP files from the specified directory. - - -

A Complete Walkthrough

-To better understand how everything ties together, from start to finish, we will walk through a complete real world example audit. This example will touch on many intermediate to advanced Sulley concepts and should hopefully solidify your understanding of the framework. Many details regarding the specifics of the target are skipped in this walkthrough, the main purpose of this section is to demonstrate the usage of a number of advanced Sulley features. The chosen target is Trend Micro Server Protect, specifically a Microsoft DCE/RPC endpoint on TCP port 5168 bound to by the service SpntSvc.exe. The RPC endpoint is exposed from TmRpcSrv.dll with the following Interface Definition Language (IDL) stub information: -
-    // opcode: 0x00, address: 0x65741030
-    // uuid: 25288888-bd5b-11d1-9d53-0080c83a5c2c
-    // version: 1.0
-
-    error_status_t rpc_opnum_0 (
-    [in] handle_t arg_1,                          // not sent on wire
-    [in] long trend_req_num,
-    [in][size_is(arg_4)] byte some_string[],
-    [in] long arg_4,
-    [out][size_is(arg_6)] byte arg_5[],           // not sent on wire
-    [in] long arg_6
-    );
-
-Neither the parameters 'arg_1' nor 'arg_6' are actually transmitted across the wire. This is an important fact to consider later when we write the actual fuzz requests. Further examination reveals that the parameter 'trend_req_num' has special meaning. The upper and lower halves of this parameter control a pair of jump tables which expose a plethora of reachable sub-routines through this single RPC function. Reverse engineering the jump tables reveals the following combinations: -
    -
  • When the value for the upper half is 0x0001, 1 through 21 are valid lower half values. -
  • When the value for the upper half is 0x0002, 1 through 18 are valid lower half values. -
  • When the value for the upper half is 0x0003, 1 through 84 are valid lower half values. -
  • When the value for the upper half is 0x0005, 1 through 24 are valid lower half values. -
  • When the value for the upper half is 0x000A, 1 through 48 are valid lower half values. -
  • When the value for the upper half is 0x001F, 1 through 24 are valid lower half values. -
-We must next create a custom encoder routine which will be responsible for encapsulating defined blocks as a valid DCE/RPC request. There is only a single function number, so this is simple. We define a basic wrapper around utisl.dcerpc.request() which hard codes the opcode parameter to zero: -
-# dce rpc request encoder used for trend server protect 5168 RPC service.
-# opnum is always zero.
-def rpc_request_encoder (data):
-    return utils.dcerpc.request(0, data)
-
-

Building the Requests

-Armed with this information and our encoder we can begin to define our Sulley requests. We create a file 'requests\trend.py' to contain all our Trend related request and helper definitions and begin coding. This is an excellent example of how building a fuzzer request within a language (as opposed to a custom language) is beneficial as we take advantage of some Python looping to automatically generate a separate request for each valid upper value from 'trend_req_num': -
-for op, submax in [(0x1, 22), (0x2, 19), (0x3, 85), (0x5, 25), (0xa, 49), (0x1f, 25)]:
-    s_initialize("5168: op-%x" % op)
-    if s_block_start("everything", encoder=rpc_request_encoder):
-        # [in] long trend_req_num,
-        s_group("subs", values=map(chr, range(1, submax)))
-        s_static("\x00")                 # subs is actually a little endian word
-        s_static(struct.pack("<H", op))  # opcode
-
-        # [in][size_is(arg_4)] byte some_string[],
-        s_size("some_string")
-        if s_block_start("some_string", group="subs"):
-            s_static("A" * 0x5000, name="arg3")
-        s_block_end()
-
-        # [in] long arg_4,
-        s_size("some_string")
-
-        # [in] long arg_6
-        s_static(struct.pack("<L", 0x5000)) # output buffer size
-    s_block_end()
-
-Within each generated request a new block is initialized and passed our previously defined custom encoder. Next, the s_group() primitive is used to define a sequence named 'subs' which represents the lower half value of 'trend_req_num' we saw earlier. The upper half word value is next added to the request stream as a static value. We will not be fuzzing the 'trend_req_num' as we have reverse engineered its valid values, had we not, we could enable fuzzing for these fields as well. Next, the NDR size prefix for 'some_string' is added to the request. We could optionally use the Sulley DCE/RPC NDR Lego primitives here, but since the RPC request is so simple we decide to represent the NDR format manually. Next, the 'some_string' value is added to the request. The string value is encapsulated in a block so that it's length can be measured. In this case we use a static sized string of the character 'A' (roughly 20k worth). Normally we would insert an s_string() primitive here, but since we know Trend will crash with any long string we reduce the test set by utilizing a static value. The length of the string is appended to the request again to fullfill the size_is requirement for 'arg_4'. Finally, we specify an arbitrary static size for the output buffer size and close the block. Our requests are now ready and we can move on to creating a session. -

Creating the Session

-We create a new file in the top level Sulley folder named 'fuzz_trend_server_protect_5168.py' for our session. This file has since been moved to the 'archived_fuzzies' folder since it has completed its life. First thing's first, we import Sulley and the created Trend requests from the request library: -
-from sulley   import *
-from requests import trend
-
-Next, we are going to define a pre-send function which is responsible for establishing the DCE/RPC connection prior to the transmission of any individual test case. The pre-send routine accepts a single parameter, the socket to transmit data on. This is a simple routine to write thanks to the availability of utils.dcerpc.bind(), a Sulley utility routine: -
-def rpc_bind (sock):
-    bind = utils.dcerpc.bind("25288888-bd5b-11d1-9d53-0080c83a5c2c", "1.0")
-    sock.send(bind)
-
-    utils.dcerpc.bind_ack(sock.recv(1000))
-
-Now it's time to initiate the session and define a target. We'll fuzz a single target, an installation of Trend Server Protect housed inside a VMWare virtual machine with the address 10.0.0.1. We'll follow the framework guidelines by saving the serialized session information to the 'audits' directory. Finally, we register a network monitor, process monitor and virtual machine control agent with the defined target: -
-sess   = sessions.session(session_filename="audits/trend_server_protect_5168.session")
-target = sessions.target("10.0.0.1", 5168)
-
-target.netmon    = pedrpc.client("10.0.0.1",  26001)
-target.procmon   = pedrpc.client("10.0.0.1",  26002)
-target.vmcontrol = pedrpc.client("127.0.0.1", 26003)
-
-Since a VMWare control agent is present, Sulley will default to reverting to a known good snapshot whenever a fault is detected or the target is unable to be reached. If a VMWare control agent is not available but a process monitor agent is, then Sulley attempts to restart the target process to resume fuzzing. This is accomplished by specifying the 'stop_commands' and 'start_commands' options to the process monitor agent: -
-target.procmon_options = \
-{
-    "proc_name"      : "SpntSvc.exe",
-    "stop_commands"  : ['net stop "trend serverprotect"'],
-    "start_commands" : ['net start "trend serverprotect"'],
-}
-
-The 'proc_name' parameter is mandatory whenever you use the process monitor agent, it specifies what process name the debugger should attach to and look for faults in. If neither a VMWare control agent nor a process monitor agent are available, then Sulley has no choice but to simply provide the target time to recover in the event a data transmission is unsuccessful. -

-Next, we instruct the target to start by calling the VMWare control agents restart_target() routine. Once running, the target is added to the session, the pre-send routine is defined and each of the defined requests is connected to the root fuzzing node. Finally, fuzzing commences with a call to the session classes fuzz() routine. -
-# start up the target.
-target.vmcontrol.restart_target()
-
-print "virtual machine up and running"
-
-sess.add_target(target)
-sess.pre_send = rpc_bind
-sess.connect(s_get("5168: op-1"))
-sess.connect(s_get("5168: op-2"))
-sess.connect(s_get("5168: op-3"))
-sess.connect(s_get("5168: op-5"))
-sess.connect(s_get("5168: op-a"))
-sess.connect(s_get("5168: op-1f"))
-sess.fuzz()
-
-

Setting up the Environment

-The final step before launching the fuzz session is to setup the environment. We do so by bringing up the target virtual machine image and launching the network and process monitor agents directly within the test image with the following command line parameters: -
-network_monitor.py -d 1 \
-                   -f "src or dst port 5168" \
-                   -p audits\trend_server_protect_5168
-
-process_monitor.py -c audits\trend_server_protect_5168.crashbin \
-                   -p SpntSvc.exe
-
-Both agents are executed from a mapped share which corresponds with the Sulley top-level directory where the session script is running from. A BPF filter string is passed to the network monitor to ensure that only the packets we are interested in are recorded. A directory within the audits folder is also chosen where the network monitor will create PCAPs for every test case. With both agents and the target process running a live snapshot is made as named "sulley ready and waiting". -

-Next, we shut down VMWare and launch the VMWare control agent on the host system (the fuzzing system). This agent requires the path to the vmrun.exe executable, the path to the actual image to control and finally the name of the snapshot to revert to in the event of a fault discovery of data transmission failure: -
-vmcontrol.py -r "c:\Progra~1\VMware\VMware~1\vmrun.exe" \
-             -x "v:\vmfarm\images\windows\2000\win_2000_pro-clones\TrendM~1\win_2000_pro.vmx" \
-             --snapshot "sulley ready and waiting"
-
-

Ready, Set ... Action! ... and post mortem.

-Finally, we are ready. Simply launch 'fuzz_trend_server_protect_5168.py', connect a web browser to http://127.0.0.1:26000 to monitor the fuzzer progress, sit back, watch and enjoy. -

-When the fuzzer completes running through its list of 221 test cases we discover that 19 of them triggered faults. Using the 'crashbin_explorer.py' utility we can explore the faults categorized by exception address: -
-$ ./utils/crashbin_explorer.py audits/trend_server_protect_5168.crashbin
-    [6] [INVALID]:41414141 Unable to disassemble at 41414141 from thread 568 caused access violation
-            42, 109, 156, 164, 170, 198,
-    [3] LogMaster.dll:63272106 push ebx from thread 568 caused access violation
-            53, 56, 151,
-    [1] ntdll.dll:77fbb267 push dword [ebp+0xc] from thread 568 caused access violation
-            195,
-    [1] Eng50.dll:6118954e rep movsd from thread 568 caused access violation
-            181,
-    [1] ntdll.dll:77facbbd push edi from thread 568 caused access violation
-            118,
-    [1] Eng50.dll:61187671 cmp word [eax],0x3b from thread 568 caused access violation
-            116,
-    [1] [INVALID]:0058002e Unable to disassemble at 0058002e from thread 568 caused access violation
-            70,
-    [2] Eng50.dll:611896d1 rep movsd from thread 568 caused access violation
-            152, 182,
-    [1] StRpcSrv.dll:6567603c push esi from thread 568 caused access violation
-            106,
-    [1] KERNEL32.dll:7c57993a cmp ax,[edi] from thread 568 caused access violation
-            165,
-    [1] Eng50.dll:61182415 mov edx,[edi+0x20c] from thread 568 caused access violation
-            50,
-
-Some of these are clearly exploitable issues. The test cases that resulted with an EIP of 0x41414141 for example. Test case 70 seems to have stumbled upon a possible code execution issue as well, a UNICODE overflow (actually this can be a straight overflow with a bit more research). The crash bin explorer utility can generate a graph view of the detected faults as well, drawing paths based on observed stack backtraces. This can help pin point the root cause of certain issues. The utility accepts the following command line arguments: -
-$ ./utils/crashbin_explorer.py
-    USAGE: crashbin_explorer.py <xxx.crashbin>
-        [-t|--test #]     dump the crash synopsis for a specific test case number
-        [-g|--graph name] generate a graph of all crash paths, save to 'name'.udg
-
-We can for example further examine the CPU state at the time of the fault detected in response to test case #70: -
-$ ./utils/crashbin_explorer.py audits/trend_server_protect_5168.crashbin -t 70
-    [INVALID]:0058002e Unable to disassemble at 0058002e from thread 568 caused access violation
-    when attempting to read from 0x0058002e
-    CONTEXT DUMP
-      EIP: 0058002e Unable to disassemble at 0058002e
-      EAX: 00000001 (         1) -> N/A
-      EBX: 0259e118 (  39444760) -> A.....AAAAA (stack)
-      ECX: 00000000 (         0) -> N/A
-      EDX: ffffffff (4294967295) -> N/A
-      EDI: 00000000 (         0) -> N/A
-      ESI: 0259e33e (  39445310) -> A.....AAAAA (stack)
-      EBP: 00000000 (         0) -> N/A
-      ESP: 0259d594 (  39441812) -> LA.XLT.......MPT.MSG.OFT.PPS.RT (stack)
-      +00: 0041004c (   4259916) -> N/A
-      +04: 0058002e (   5767214) -> N/A
-      +08: 0054004c (   5505100) -> N/A
-      +0c: 0056002e (   5636142) -> N/A
-      +10: 00530042 (   5439554) -> N/A
-      +14: 004a002e (   4849710) -> N/A
-    disasm around:
-            0x0058002e Unable to disassemble
-    SEH unwind:
-            0259fc58 -> StRpcSrv.dll:656784e3
-            0259fd70 -> TmRpcSrv.dll:65741820
-            0259fda8 -> TmRpcSrv.dll:65741820
-            0259ffdc -> RPCRT4.dll:77d87000
-            ffffffff -> KERNEL32.dll:7c5c216c
-
-You can see here that the stack has been blown away by what appears to be a UNICODE string of file extensions. You can pull up the archived PCAP file for the given test case as well. Here is an excerpt of a screenshot from Wireshark examining the contents of one of the captured PCAP files: -



-A final step we may wish to take is to remove all PCAP files which do not contain information regarding a fault. The 'pcap_cleaner.py' utility was written for exactly this task: -
-$ ./utils/pcap_cleaner.py
-    USAGE: pcap_cleaner.py <xxx.crashbin> <path to pcaps>
-
-This utility will open the specified crashbin file, read in the list of test cases numbers that triggered a fault and erase all other PCAP files from the specified directory. The discovered code execution vulnerabilities in this fuzz were all reported to Trend and have resulted in the following advisories: - -This is not to say that all possible vulnerabilities have been exhausted in this interface. In fact, this was the most rudimentary fuzzing possible of this interface. A secondary fuzz which actually uses the s_string() primitive as opposed to simply a long string can now be beneficial. - - -

Upcoming Features

-

Feedback Loop

-A new monitoring agent is in works that will monitor individual instruction execution in search of comparisons to constant values. These "hints" will get passed back to the fuzz session for inclusion in data generation and hopes of improved code coverage. - -

Parallel Fuzzing

-The current "architecture" is designed to allow for the addition of multiple targets each with its own configuration of monitoring agents. The completion of parallel fuzzing will disperse the total fuzz set workload across all available target to drastically improve performance. - -

Sequence Honing

-A common problem that arrises with fuzzing is that replaying a test case previously deemed to generate a fault does not reproduce the issue. The likely reasoning behind this is that some sequence of previously transmitted test cases put the target in a vulnerable state. The honing tool will apply a sliding / splitting window algorithm to determine the exact sequence of events necessary to reproduce a given issue. - -

Code Coverage Monitor

-A new monitoring agent is in the works that will record code coverage in reaction to each individually transmitted test case. This will assist in determining the exact source of detected faults as well as provide an indicator as to the effectiveness of the fuzzing session. - -

PCAP Binning

-Similar to the crash binning functionality which handles automatic categorization of fault addresses and paths, the PCAP binning tool will automatically categorize observed responses to fuzz data. - -
-
- - \ No newline at end of file diff --git a/docs/introducing_sulley.pdf b/docs/introducing_sulley.pdf deleted file mode 100755 index c8b1ff0..0000000 Binary files a/docs/introducing_sulley.pdf and /dev/null differ diff --git a/docs/stylesheet.css b/docs/stylesheet.css deleted file mode 100644 index 9114c77..0000000 --- a/docs/stylesheet.css +++ /dev/null @@ -1,59 +0,0 @@ -body -{ - font-family: arial, helvetica, sans-serif; - font-size: 12px; - color: #000000; -} - -a:link {color: #990000; text-decoration: underline;} -a:visited {color: #990000; text-decoration: underline;} -a:hover {color: #FFFFFF; text-decoration: none; background-color: #990000} -a:active {color: #990000; text-decoration: underline;} - -td -{ - font-family: arial, helvetica, sans-serif; - font-size: 12px; - color: #000000; -} - -h1 -{ - font-family: arial, helvetica, sans-serif; - font-size: 35px; - color: #FFFFFF; - background-color: #004080; - margin-top: 10px; -} - -h2 -{ - font-family: arial, helvetica, sans-serif; - font-size: 20px; - color: #004080; - border-bottom: 1px solid #B0C8E3; - margin-top: 30px; - margin-bottom: 10px; -} - -h3 -{ - font-family: arial, helvetica, sans-serif; - font-size: 15px; - color: #005BB7; - margin-bottom: 5px; -} - -.code -{ - border: 2px dashed #cccccc; - padding: 10px; - margin: 10px; - margin-left: 5%; - margin-right: 5%; - background-color: #D2E9FF; - color: #000000; - font-size: 12px; - font-family: courier new; - word-wrap: break-word; -} \ No newline at end of file diff --git a/examples/fuzz_trend_control_manager_20901.py b/examples/fuzz_trend_control_manager_20901.py index 2f41970..879c952 100644 --- a/examples/fuzz_trend_control_manager_20901.py +++ b/examples/fuzz_trend_control_manager_20901.py @@ -6,12 +6,13 @@ # this was a really half assed fuzz. someone should take it further, see my notes in the requests file for more info. # -from sulley import * +from sulley import * +# noinspection PyUnresolvedReferences from requests import trend -######################################################################################################################## -sess = sessions.session(session_filename="audits/trend_server_protect_20901.session", sleep_time=.25, log_level=10) -sess.add_target(sessions.target("192.168.181.2", 20901)) + +sess = sessions.Session(session_filename="audits/trend_server_protect_20901.session", sleep_time=.25, log_level=10) +sess.add_target(sessions.Target("192.168.181.2", 20901)) sess.connect(s_get("20901")) sess.fuzz() diff --git a/examples/fuzz_trend_server_protect_5168.py b/examples/fuzz_trend_server_protect_5168.py index 330221f..7ce6fde 100644 --- a/examples/fuzz_trend_server_protect_5168.py +++ b/examples/fuzz_trend_server_protect_5168.py @@ -9,7 +9,9 @@ # network_monitor.py -d 1 -f "src or dst port 5168" -p audits\trend_server_protect_5168 # # on localhost: -# vmcontrol.py -r "c:\Progra~1\VMware\VMware~1\vmrun.exe" -x "v:\vmfarm\images\windows\2000\win_2000_pro-clones\TrendM~1\win_2000_pro.vmx" --snapshot "sulley ready and waiting" +# vmcontrol.py -r "c:\Progra~1\VMware\VMware~1\vmrun.exe" \ +# -x "v:\vmfarm\images\windows\2000\win_2000_pro-clones\TrendM~1\win_2000_pro.vmx" \ +# --snapshot "sulley ready and waiting" # # this key gets written which fucks trend service even on reboot. # HKEY_LOCAL_MACHINE\SOFTWARE\TrendMicro\ServerProtect\CurrentVersion\Engine @@ -17,23 +19,23 @@ # uncomment the req/num to do a single test case. # -import time - -from sulley import * +from sulley import utils, s_get, s_mutate, s_render, sessions, pedrpc +# noinspection PyUnresolvedReferences from requests import trend req = num = None #req = "5168: op-3" #num = "\x04" -def rpc_bind (sock): + +def rpc_bind(sock): bind = utils.dcerpc.bind("25288888-bd5b-11d1-9d53-0080c83a5c2c", "1.0") sock.send(bind) utils.dcerpc.bind_ack(sock.recv(1000)) -def do_single (req, num): +def do_single(req, num): import socket # connect to the server. @@ -56,20 +58,20 @@ def do_single (req, num): print "done." -def do_fuzz (): - sess = sessions.session(session_filename="audits/trend_server_protect_5168.session") - target = sessions.target("192.168.181.133", 5168) +def do_fuzz(): + sess = sessions.Session(session_filename="audits/trend_server_protect_5168.session") + target = sessions.Target("192.168.181.133", 5168) - target.netmon = pedrpc.client("192.168.181.133", 26001) - target.procmon = pedrpc.client("192.168.181.133", 26002) - target.vmcontrol = pedrpc.client("127.0.0.1", 26003) + target.netmon = pedrpc.Client("192.168.181.133", 26001) + target.procmon = pedrpc.Client("192.168.181.133", 26002) + target.vmcontrol = pedrpc.Client("127.0.0.1", 26003) target.procmon_options = \ - { - "proc_name" : "SpntSvc.exe", - "stop_commands" : ['net stop "trend serverprotect"'], - "start_commands" : ['net start "trend serverprotect"'], - } + { + "proc_name": "SpntSvc.exe", + "stop_commands": ['net stop "trend serverprotect"'], + "start_commands": ['net start "trend serverprotect"'], + } # start up the target. target.vmcontrol.restart_target() @@ -88,7 +90,6 @@ def do_fuzz (): print "done fuzzing. web interface still running." - if not req or not num: do_fuzz() else: diff --git a/examples/fuzz_trillian_jabber.py b/examples/fuzz_trillian_jabber.py index 8bf094b..3f1396c 100644 --- a/examples/fuzz_trillian_jabber.py +++ b/examples/fuzz_trillian_jabber.py @@ -9,29 +9,36 @@ # process_monitor.py -c audits\trillian_jabber.crashbin -p trillian.exe # # on localhost: -# vmcontrol.py -r "c:\Progra~1\VMware\VMware~1\vmrun.exe" -x "v:\vmfarm\images\windows\xp\win_xp_pro-clones\allsor~1\win_xp_pro.vmx" --snapshot "sulley ready and waiting" +# vmcontrol.py -r "c:\Progra~1\VMware\VMware~1\vmrun.exe" \ +# -x "v:\vmfarm\images\windows\xp\win_xp_pro-clones\allsor~1\win_xp_pro.vmx" \ +# --snapshot "sulley ready and waiting" # # note: # you MUST register the IP address of the fuzzer as a valid MDNS "presence" host. to do so, simply install and # launch trillian on the fuzz box with rendezvous enabled. otherwise the target will drop the connection. # -from sulley import * +from sulley import sessions, \ + pedrpc, \ + s_get + +# noinspection PyUnresolvedReferences from requests import jabber -def init_message (sock): + +def init_message(sock): init = '\n' init += '' sock.send(init) sock.recv(1024) -sess = sessions.session(session_filename="audits/trillian.session") -target = sessions.target("152.67.137.126", 5298) -target.netmon = pedrpc.client("152.67.137.126", 26001) -target.procmon = pedrpc.client("152.67.137.126", 26002) -target.vmcontrol = pedrpc.client("127.0.0.1", 26003) -target.procmon_options = { "proc_name" : "trillian.exe" } +sess = sessions.Session(session_filename="audits/trillian.session") +target = sessions.Target("152.67.137.126", 5298) +target.netmon = pedrpc.Client("152.67.137.126", 26001) +target.procmon = pedrpc.Client("152.67.137.126", 26002) +target.vmcontrol = pedrpc.Client("127.0.0.1", 26003) +target.procmon_options = {"proc_name": "trillian.exe"} # start up the target. target.vmcontrol.restart_target() diff --git a/examples/mdns.py b/examples/mdns.py index 77da174..7423012 100755 --- a/examples/mdns.py +++ b/examples/mdns.py @@ -3,13 +3,23 @@ # A partial MDNS fuzzer. Could be made to be a DNS fuzzer trivially # Charlie Miller -from sulley import * -from binascii import * -from struct import * +from sulley import s_word, \ + s_initialize, \ + sessions, \ + s_block_start, \ + s_size, \ + s_block_end, \ + s_string, \ + s_repeat, \ + s_group, \ + s_dword, \ + s_binary, \ + s_get -def insert_questions (sess, node, edge, sock): - node.names['Questions'].value = 1+node.names['queries'].current_reps - node.names['Authority'].value = 1+node.names['auth_nameservers'].current_reps + +def insert_questions(sess, node, edge, sock): + node.names['Questions'].value = 1 + node.names['queries'].current_reps + node.names['Authority'].value = 1 + node.names['auth_nameservers'].current_reps s_initialize("query") s_word(0, name="TransactionID") @@ -19,50 +29,50 @@ def insert_questions (sess, node, edge, sock): s_word(1, name="Authority", endian='>') s_word(0, name="Additional", endian='>') -######### Queries ################ +# ######## Queries ################ if s_block_start("query"): - if s_block_start("name_chunk"): - s_size("string", length=1) - if s_block_start("string"): - s_string("A"*10) - s_block_end() - s_block_end() - s_repeat("name_chunk", min_reps=2, max_reps=4, step=1, fuzzable=True, name="aName") + if s_block_start("name_chunk"): + s_size("string", length=1) + if s_block_start("string"): + s_string("A" * 10) + s_block_end() + s_block_end() + s_repeat("name_chunk", min_reps=2, max_reps=4, step=1, fuzzable=True, name="aName") - s_group("end", values=["\x00", "\xc0\xb0"]) # very limited pointer fuzzing - s_word(0xc, name="Type", endian='>') - s_word(0x8001, name="Class", endian='>') + s_group("end", values=["\x00", "\xc0\xb0"]) # very limited pointer fuzzing + s_word(0xc, name="Type", endian='>') + s_word(0x8001, name="Class", endian='>') s_block_end() s_repeat("query", 0, 1000, 40, name="queries") ######## Authorities ############ if s_block_start("auth_nameserver"): - if s_block_start("name_chunk_auth"): - s_size("string_auth", length=1) - if s_block_start("string_auth"): - s_string("A"*10) - s_block_end() - s_block_end() - s_repeat("name_chunk_auth", min_reps=2, max_reps=4, step=1, fuzzable=True, name="aName_auth") - s_group("end_auth", values=["\x00", "\xc0\xb0"]) # very limited pointer fuzzing + if s_block_start("name_chunk_auth"): + s_size("string_auth", length=1) + if s_block_start("string_auth"): + s_string("A" * 10) + s_block_end() + s_block_end() + s_repeat("name_chunk_auth", min_reps=2, max_reps=4, step=1, fuzzable=True, name="aName_auth") + s_group("end_auth", values=["\x00", "\xc0\xb0"]) # very limited pointer fuzzing - s_word(0xc, name="Type_auth", endian='>') - s_word(0x8001, name="Class_auth", endian='>') - s_dword(0x78, name="TTL_auth", endian='>') - s_size("data_length", length=2, endian='>') - if s_block_start("data_length"): - s_binary("00 00 00 00 00 16 c0 b0") # This should be fuzzed according to the type, but I'm too lazy atm - s_block_end() + s_word(0xc, name="Type_auth", endian='>') + s_word(0x8001, name="Class_auth", endian='>') + s_dword(0x78, name="TTL_auth", endian='>') + s_size("data_length", length=2, endian='>') + if s_block_start("data_length"): + s_binary("00 00 00 00 00 16 c0 b0") # This should be fuzzed according to the type, but I'm too lazy atm + s_block_end() s_block_end() s_repeat("auth_nameserver", 0, 1000, 40, name="auth_nameservers") s_word(0) -sess = sessions.session(proto="udp") -target = sessions.target("224.0.0.251", 5353) +sess = sessions.Session(proto="udp") +target = sessions.Target("224.0.0.251", 5353) sess.add_target(target) -sess.connect(s_get("query"), callback=insert_questions ) +sess.connect(s_get("query"), callback=insert_questions) sess.fuzz() diff --git a/installer/Nullsoft Installer Script.nsi b/installer/Nullsoft Installer Script.nsi deleted file mode 100644 index f8c7cec..0000000 --- a/installer/Nullsoft Installer Script.nsi +++ /dev/null @@ -1,128 +0,0 @@ -; Sulley Fuzzing Framework Installer -; Aaron Portnoy -; TippingPoint Security Research Team -; (c) 2007 - -; HM NIS Edit Wizard helper defines -!define PRODUCT_NAME "Sulley Fuzzing Framework" -!define PRODUCT_VERSION "1.0" -!define PRODUCT_PUBLISHER "Pedram Amini and Aaron Portnoy" -!define PRODUCT_WEB_SITE "http://www.fuzzing.org" -!define PRODUCT_DIR_REGKEY "Software\Microsoft\Windows\CurrentVersion\App Paths\Sulley.exe" -!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" -!define PRODUCT_UNINST_ROOT_KEY "HKLM" - -; MUI 1.67 compatible ------ -!include "MUI.nsh" - -; ZIP support -!include "ZipDLL.nsh" - -!define MUI_ABORTWARNING - -; icons -!define MUI_ICON "..\..\sulley_icon.ico" -!define MUI_UNICON "..\..\sulley_icon.ico" - -; Welcome page -!insertmacro MUI_PAGE_WELCOME -; License page -!insertmacro MUI_PAGE_LICENSE "..\LICENSE.txt" -; Directory page -!insertmacro MUI_PAGE_DIRECTORY -; Instfiles page -!insertmacro MUI_PAGE_INSTFILES -; Finish page -!define MUI_FINISHPAGE_RUN -; Run on completion -!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchDocsAndShell" -!insertmacro MUI_PAGE_FINISH -; Uninstaller pages -!insertmacro MUI_UNPAGE_INSTFILES -; Language files -!insertmacro MUI_LANGUAGE "English" - - - -Function LaunchDocsAndShell - ExecShell "" "$INSTDIR\docs\index.html" - Exec 'cmd.exe /c cd "$INSTDIR"' -FunctionEnd - -Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" -OutFile "Sulley Fuzzing Framework.exe" -InstallDir "$PROGRAMFILES\Sulley Fuzzing Framework" -InstallDirRegKey HKLM "${PRODUCT_DIR_REGKEY}" "" -ShowInstDetails show -ShowUnInstDetails show - - -Section "Sulley" SEC01 - SetOutPath "$INSTDIR\install_files" - File "install_files\python.msi" - File "install_files\winpcap.exe" - File "install_files\pcapy.exe" - File "install_files\ctypes.exe" - SetOutPath "$INSTDIR" - File "sulley.zip" - ZipDLL::extractall "$INSTDIR\sulley.zip" "$INSTDIR" -SectionEnd - - -Section "Python" SEC02 - SetOverwrite ifnewer - ExecWait 'msiexec /i "$INSTDIR\install_files\python.msi"' -SectionEnd - -Section "Pcapy" SEC03 - SetOverwrite ifnewer - ExecWait "$INSTDIR\install_files\pcapy.exe" -SectionEnd - -Section "WinPCAP" SEC04 - SetOverwrite ifnewer - ExecWait "$INSTDIR\install_files\winpcap.exe" -SectionEnd - -Section "ctypes" SEC05 - SetOverwrite ifnewer - ExecWait "$INSTDIR\install_files\ctypes.exe" -SectionEnd - - -Section -AdditionalIcons - SetOutPath $INSTDIR - WriteIniStr "$INSTDIR\${PRODUCT_NAME}.url" "InternetShortcut" "URL" "${PRODUCT_WEB_SITE}" - CreateShortCut "$SMPROGRAMS\Sulley Fuzzing Framework\Website.lnk" "$INSTDIR\${PRODUCT_NAME}.url" - CreateShortCut "$SMPROGRAMS\Sulley Fuzzing Framework\Uninstall.lnk" "$INSTDIR\uninst.exe" -SectionEnd - -Section -Post - WriteUninstaller "$INSTDIR\uninst.exe" - WriteRegStr HKLM "${PRODUCT_DIR_REGKEY}" "" "$PROGRAMFILES\pcapy-0.10.5.win32-py2.5.exe" - WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" - WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\uninst.exe" - WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$PROGRAMFILES\pcapy-0.10.5.win32-py2.5.exe" - WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}" - WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}" - WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}" -SectionEnd - - -Function un.onUninstSuccess - HideWindow - MessageBox MB_ICONINFORMATION|MB_OK "Sulley was successfully removed from your computer." -FunctionEnd - -Function un.onInit - MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "Are you sure you want to completely remove Sulley and all of its components?" IDYES +2 - Abort -FunctionEnd - -Section Uninstall - RMDir /r "$INSTDIR" - - DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" - DeleteRegKey HKLM "${PRODUCT_DIR_REGKEY}" - SetAutoClose true -SectionEnd \ No newline at end of file diff --git a/network_monitor.py b/network_monitor.py index 9512fd8..e803f87 100644 --- a/network_monitor.py +++ b/network_monitor.py @@ -4,6 +4,7 @@ import time import sys import os +# noinspection PyUnresolvedReferences import pcapy import impacket import impacket.ImpactDecoder @@ -35,9 +36,9 @@ def create_usage(): try: # extract the device UUID and open the TCP/IP parameters key for it. - pcapy_device = pcapy_device[pcapy_device.index("{"):pcapy_device.index("}") + 1] + pcapy_device = pcapy_device[pcapy_device.index("{"):pcapy_device.index("}") + 1] subkey = r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\%s" % pcapy_device - key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, subkey) + key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, subkey) # if there is a DHCP address snag that, otherwise fall back to the IP address. try: @@ -53,15 +54,14 @@ def create_usage(): return message -######################################################################################################################## -class PcapThread (threading.Thread): - def __init__ (self, network_monitor, pcap, pcap_save_path): +class PcapThread(threading.Thread): + def __init__(self, network_monitor, pcap, pcap_save_path): self.network_monitor = network_monitor - self.pcap = pcap - self.decoder = None - self.dumper = self.pcap.dump_open(pcap_save_path) - self.active = True - self.data_bytes = 0 + self.pcap = pcap + self.decoder = None + self.dumper = self.pcap.dump_open(pcap_save_path) + self.active = True + self.data_bytes = 0 # register the appropriate decoder. if pcap.datalink() == pcapy.DLT_EN10MB: @@ -73,7 +73,7 @@ def __init__ (self, network_monitor, pcap, pcap_save_path): threading.Thread.__init__(self) - def packet_handler (self, header, data): + def packet_handler(self, header, data): # add the captured data to the PCAP. self.dumper.dump(header, data) @@ -83,15 +83,14 @@ def packet_handler (self, header, data): # log the decoded data at the appropriate log level. self.network_monitor.log(self.decoder.decode(data), 15) - def run (self): + def run(self): # process packets while the active flag is raised. while self.active: self.pcap.dispatch(0, self.packet_handler) -######################################################################################################################## -class NetworkMonitorPedrpcServer (pedrpc.server): - def __init__ (self, host, port, monitor_device, bpf_filter="", path="./", level=1): +class NetworkMonitorPedrpcServer(pedrpc.Server): + def __init__(self, host, port, monitor_device, bpf_filter="", path="./", level=1): """ @type host: str @param host: Hostname or IP address to bind server to @@ -108,12 +107,12 @@ def __init__ (self, host, port, monitor_device, bpf_filter="", path="./", level= """ # initialize the PED-RPC server. - pedrpc.server.__init__(self, host, port) - self.device = monitor_device - self.filter = bpf_filter - self.log_path = path - self.log_level = level - self.pcap = None + pedrpc.Server.__init__(self, host, port) + self.device = monitor_device + self.filter = bpf_filter + self.log_path = path + self.log_level = level + self.pcap = None self.pcap_thread = None # ensure the log path is valid. @@ -128,7 +127,7 @@ def __init__ (self, host, port, monitor_device, bpf_filter="", path="./", level= self.log("\t log_level: %d" % self.log_level) self.log("Awaiting requests...") - def __stop (self): + def __stop(self): """ Kill the PCAP thread. """ @@ -137,16 +136,17 @@ def __stop (self): self.log("stopping active packet capture thread.", 10) self.pcap_thread.active = False - self.pcap_thread = None + self.pcap_thread = None - def alive (self): + # noinspection PyMethodMayBeStatic + def alive(self): """ Returns True. Useful for PED-RPC clients who want to see if the PED-RPC connection is still alive. """ return True - def post_send (self): + def post_send(self): """ This routine is called after the fuzzer transmits a test case and returns the number of bytes captured by the PCAP thread. @@ -164,7 +164,7 @@ def post_send (self): self.log("stopped PCAP thread, snagged %d bytes of data" % data_bytes) return data_bytes - def pre_send (self, test_number): + def pre_send(self, test_number): """ This routine is called before the fuzzer transmits a test case and spin off a packet capture thread. """ @@ -180,7 +180,7 @@ def pre_send (self, test_number): self.pcap_thread = PcapThread(self, self.pcap, pcap_log_path) self.pcap_thread.start() - def log (self, msg="", level=1): + def log(self, msg="", level=1): """ If the supplied message falls under the current log level, print the specified message to screen. @@ -191,7 +191,7 @@ def log (self, msg="", level=1): if self.log_level >= level: print "[%s] %s" % (time.strftime("%I:%M.%S"), msg) - def retrieve (self, test_number): + def retrieve(self, test_number): """ Return the raw binary contents of the PCAP saved for the specified test case number. @@ -202,23 +202,21 @@ def retrieve (self, test_number): self.log("retrieving PCAP for test case #%d" % test_number) pcap_log_path = "%s/%d.pcap" % (self.log_path, test_number) - fh = open(pcap_log_path, "rb") - data = fh.read() + fh = open(pcap_log_path, "rb") + data = fh.read() fh.close() return data - def set_filter (self, new_filter): + def set_filter(self, new_filter): self.log("updating PCAP filter to '%s'" % new_filter) self.filter = new_filter - def set_log_path (self, new_log_path): + def set_log_path(self, new_log_path): self.log("updating log path to '%s'" % new_log_path) self.log_path = new_log_path -######################################################################################################################## - if __name__ == "__main__": usage_message = create_usage() rpc_port = 26001 @@ -231,10 +229,10 @@ def set_log_path (self, new_log_path): except getopt.GetoptError: log_error(usage_message) - device = None + device = None pcap_filter = "" - log_path = "./" - log_level = 1 + log_path = "./" + log_level = 1 for opt, arg in opts: if opt in ("-d", "--device"): diff --git a/new_examples/fuzz_http_server.py b/new_examples/fuzz_http_server.py new file mode 100644 index 0000000..5fb627a --- /dev/null +++ b/new_examples/fuzz_http_server.py @@ -0,0 +1,77 @@ +import sys +sys.path.insert(0, '../') + +from sulley.primitives import String, Static, Delim + + +class Group(object): + blocks = [] + + def __init__(self, name, definition=None): + self.name = name + if definition: + self.definition = definition + + def add_definition(self, definition): + assert isinstance(definition, (list, tuple)), "Definition must be a list or a tuple!" + self.definition = definition + + def render(self): + return "".join([x.value for x in self.definition]) + + def exhaust(self): + for item in self.definition: + while item.mutate(): + current_value = item.value + self.log_send(current_value) + recv_data = self.send_buffer(current_value) + self.log_recv(recv_data) + + def __repr__(self): + return '<%s [%s items]>' % (self.__class__.__name__, len(self.definition)) + + # noinspection PyMethodMayBeStatic + def send_buffer(self, current_value): + return "Sent %s!" % current_value + + def log_send(self, current_value): + pass + + def log_recv(self, recv_data): + pass + + +s_static = Static +s_delim = Delim +s_string = String + +CloseHeader = Group( + "HTTP Close Header", + definition=[ + # GET / HTTP/1.1\r\n + s_static("GET / HTTP/1.1\r\n"), + # Connection: close + s_static("Connection"), s_delim(":"), s_delim(" "), s_string("close"), + s_static("\r\n\r\n") + ] +) + +OpenHeader = Group( + "HTTP Open Header", + definition=[ + # GET / HTTP/1.1\r\n + Static("GET / HTTP/1.1\r\n"), + # Connection: close + Static("Connection"), Delim(":"), Delim(" "), String("open"), + Static("\r\n\r\n") + ] +) + +# CloseHeader = Group("HTTP Close Header") +# CloseHeader.add_definition([ +# # GET / HTTP/1.1\r\n +# s_static("GET / HTTP/1.1\r\n"), +# # Connection: close +# s_static("Connection"), s_delim(":"), s_delim(" "), s_string("close"), +# s_static("\r\n\r\n") +# ]) \ No newline at end of file diff --git a/process_monitor.py b/process_monitor.py index be13aac..af3df79 100644 --- a/process_monitor.py +++ b/process_monitor.py @@ -12,8 +12,8 @@ from sulley import pedrpc -PORT = 26002 -ERR = lambda msg: sys.stderr.write("ERR> " + msg + "\n") or sys.exit(1) +PORT = 26002 +ERR = lambda msg: sys.stderr.write("ERR> " + msg + "\n") or sys.exit(1) USAGE = """USAGE: process_monitor.py <-c|--crash_bin FILENAME> filename to serialize crash bin class to [-p|--proc_name NAME] process name to search for and attach to @@ -23,23 +23,22 @@ """ -######################################################################################################################## -class DebuggerThread (threading.Thread): - def __init__ (self, process_monitor, process, pid_to_ignore=None): +class DebuggerThread(threading.Thread): + def __init__(self, process_monitor, process, pid_to_ignore=None): """ Instantiate a new PyDbg instance and register user and access violation callbacks. """ threading.Thread.__init__(self) - self.process_monitor = process_monitor - self.proc_name = process - self.ignore_pid = pid_to_ignore + self.process_monitor = process_monitor + self.proc_name = process + self.ignore_pid = pid_to_ignore self.access_violation = False - self.active = True - self.dbg = pydbg.pydbg() - self.pid = None + self.active = True + self.dbg = pydbg.pydbg() + self.pid = None # give this thread a unique name. self.setName("%d" % time.time()) @@ -50,7 +49,7 @@ def __init__ (self, process_monitor, process, pid_to_ignore=None): self.dbg.set_callback(pydbg.defines.USER_CALLBACK_DEBUG_EVENT, self.dbg_callback_user) self.dbg.set_callback(pydbg.defines.EXCEPTION_ACCESS_VIOLATION, self.dbg_callback_access_violation) - def dbg_callback_access_violation (self, dbg): + def dbg_callback_access_violation(self, dbg): """ Ignore first chance exceptions. Record all unhandled exceptions to the process monitor crash bin and kill the target process. @@ -69,7 +68,7 @@ def dbg_callback_access_violation (self, dbg): # save the the crash synopsis. self.process_monitor.last_synopsis = self.process_monitor.crash_bin.crash_synopsis() - first_line = self.process_monitor.last_synopsis.split("\n")[0] + first_line = self.process_monitor.last_synopsis.split("\n")[0] self.process_monitor.log("debugger thread-%s caught access violation: '%s'" % (self.getName(), first_line)) @@ -80,7 +79,7 @@ def dbg_callback_access_violation (self, dbg): dbg.terminate_process() return pydbg.defines.DBG_CONTINUE - def dbg_callback_user (self, dbg): + def dbg_callback_user(self, dbg): """ The user callback is run roughly every 100 milliseconds (WaitForDebugEvent() timeout from pydbg_core.py). Simply check if the active flag was lowered and if so detach from the target process. The thread should then exit. @@ -92,7 +91,7 @@ def dbg_callback_user (self, dbg): return pydbg.defines.DBG_CONTINUE - def run (self): + def run(self): """ Main thread routine, called on thread.start(). Thread exits when this routine returns. """ @@ -111,7 +110,7 @@ def run (self): # TODO: removing the following line appears to cause some concurrency issues. del self.dbg - def watch (self): + def watch(self): """ Continuously loop, watching for the target process. This routine "blocks" until the target process is found. Update self.pid when found and return. @@ -130,9 +129,8 @@ def watch (self): self.process_monitor.log("debugger thread-%s found match on pid %d" % (self.getName(), self.pid)) -######################################################################################################################## -class ProcessMonitorPedrpcServer (pedrpc.server): - def __init__ (self, host, port, crash_filename, proc=None, pid_to_ignore=None, level=1): +class ProcessMonitorPedrpcServer(pedrpc.Server): + def __init__(self, host, port, crash_filename, proc=None, pid_to_ignore=None, level=1): """ @type host: str @param host: Hostname or IP address @@ -149,20 +147,20 @@ def __init__ (self, host, port, crash_filename, proc=None, pid_to_ignore=None, l """ # initialize the PED-RPC server. - pedrpc.server.__init__(self, host, port) + pedrpc.Server.__init__(self, host, port) - self.crash_filename = crash_filename - self.proc_name = proc - self.ignore_pid = pid_to_ignore - self.log_level = level + self.crash_filename = crash_filename + self.proc_name = proc + self.ignore_pid = pid_to_ignore + self.log_level = level - self.stop_commands = [] - self.start_commands = [] - self.test_number = None - self.debugger_thread = None - self.crash_bin = utils.crash_binning.crash_binning() + self.stop_commands = [] + self.start_commands = [] + self.test_number = None + self.debugger_thread = None + self.crash_bin = utils.crash_binning.CrashBinning() - self.last_synopsis = "" + self.last_synopsis = "" if not os.access(os.path.dirname(self.crash_filename), os.X_OK): self.log("invalid path specified for crash bin: %s" % self.crash_filename) @@ -181,14 +179,15 @@ def __init__ (self, host, port, crash_filename, proc=None, pid_to_ignore=None, l self.log("\t log level: %d" % self.log_level) self.log("awaiting requests...") - def alive (self): + # noinspection PyMethodMayBeStatic + def alive(self): """ Returns True. Useful for PED-RPC clients who want to see if the PED-RPC connection is still alive. """ return True - def get_crash_synopsis (self): + def get_crash_synopsis(self): """ Return the last recorded crash synopsis. @@ -198,7 +197,7 @@ def get_crash_synopsis (self): return self.last_synopsis - def get_bin_keys (self): + def get_bin_keys(self): """ Return the crash bin keys, ie: the unique list of exception addresses. @@ -208,7 +207,7 @@ def get_bin_keys (self): return self.crash_bin.bins.keys() - def get_bin (self, binary): + def get_bin(self, binary): """ Return the crash entries from the specified bin or False if the bin key is invalid. @@ -224,7 +223,7 @@ def get_bin (self, binary): return self.crash_bin.bins[binary] - def log (self, msg="", level=1): + def log(self, msg="", level=1): """ If the supplied message falls under the current log level, print the specified message to screen. @@ -235,7 +234,7 @@ def log (self, msg="", level=1): if self.log_level >= level: print "[%s] %s" % (time.strftime("%I:%M.%S"), msg) - def post_send (self): + def post_send(self): """ This routine is called after the fuzzer transmits a test case and returns the status of the target. @@ -258,12 +257,12 @@ def post_send (self): # serialize the crash bin to disk. self.crash_bin.export_file(self.crash_filename) # for binary in self.crash_bin.bins.keys(): - # crashes += len(self.crash_bin.bins[binary]) + # crashes += len(self.crash_bin.bins[binary]) for binary, crash_list in self.crash_bin.bins.iteritems(): crashes += len(crash_list) return not av - def pre_send (self, test_number): + def pre_send(self, test_number): """ This routine is called before the fuzzer transmits a test case and ensure the debugger thread is operational. @@ -288,7 +287,7 @@ def pre_send (self, test_number): self.log("giving debugger thread 2 seconds to settle in", 5) time.sleep(2) - def start_target (self): + def start_target(self): """ Start up the target process by issuing the commands in self.start_commands. """ @@ -301,7 +300,7 @@ def start_target (self): self.log("done. target up and running, giving it 5 seconds to settle in.") time.sleep(5) - def stop_target (self): + def stop_target(self): """ Kill the current debugger thread and stop the target process by issuing the commands in self.stop_commands. """ @@ -321,21 +320,19 @@ def stop_target (self): else: os.system(command) - def set_proc_name (self, new_proc_name): + def set_proc_name(self, new_proc_name): self.log("updating target process name to '%s'" % new_proc_name) self.proc_name = new_proc_name - def set_start_commands (self, new_start_commands): + def set_start_commands(self, new_start_commands): self.log("updating start commands to: %s" % new_start_commands) self.start_commands = new_start_commands - def set_stop_commands (self, new_stop_commands): + def set_stop_commands(self, new_stop_commands): self.log("updating stop commands to: %s" % new_stop_commands) self.stop_commands = new_stop_commands -######################################################################################################################## - if __name__ == "__main__": opts = None # parse command line options. diff --git a/process_monitor_unix.py b/process_monitor_unix.py index 7f33d3b..c23ace4 100644 --- a/process_monitor_unix.py +++ b/process_monitor_unix.py @@ -46,14 +46,14 @@ ERR = lambda msg: sys.stderr.write("ERR> " + msg + "\n") or sys.exit(1) -class debugger_thread: +class DebuggerThread: def __init__(self, start_command): - ''' + """ This class isn't actually ran as a thread, only the start_monitoring method is. It can spawn/stop a process, wait for it to exit and report on the exit status/code. - ''' - + """ + self.start_command = start_command self.tokens = start_command.split(' ') self.cmd_args = [] @@ -65,18 +65,18 @@ def spawn_target(self): print self.tokens self.pid = os.spawnv(os.P_NOWAIT, self.tokens[0], self.tokens) self.alive = True - + def start_monitoring(self): - ''' + """ self.exit_status = os.waitpid(self.pid, os.WNOHANG | os.WUNTRACED) - while self.exit_status == (0, 0): + while self.exit_status == (0, 0): self.exit_status = os.waitpid(self.pid, os.WNOHANG | os.WUNTRACED) - ''' - + """ + self.exit_status = os.waitpid(self.pid, 0) # [0] is the pid self.exit_status = self.exit_status[1] - + self.alive = False def get_exit_status(self): @@ -86,58 +86,62 @@ def stop_target(self): os.kill(self.pid, signal.SIGKILL) self.alive = False - def isAlive(self): + def is_alive(self): return self.alive -######################################################################################################################## -class nix_process_monitor_pedrpc_server(pedrpc.server): - def __init__(self, host, port, crash_bin, log_level=1): - ''' - @type host: String +class NIXProcessMonitorPedrpcServer(pedrpc.Server): + def __init__(self, host, port, cbin, level=1): + """ + @type host: str @param host: Hostname or IP address - @type port: Integer + @type port: int @param port: Port to bind server to - @type crash_bin: String - @param crash_bin: Where to save monitored process crashes for analysis - - ''' - - pedrpc.server.__init__(self, host, port) - self.crash_bin = crash_bin - self.log_level = log_level - self.dbg = None + @type cbin: str + @param cbin: Where to save monitored process crashes for analysis + """ + + pedrpc.Server.__init__(self, host, port) + self.crash_bin = cbin + self.log_level = level + self.dbg = None + self.last_synopsis = None + self.test_number = 0 + self.start_commands = None + self.stop_commands = None + self.proc_name = None self.log("Process Monitor PED-RPC server initialized:") self.log("Listening on %s:%s" % (host, port)) self.log("awaiting requests...") - def alive (self): - ''' + # noinspection PyMethodMayBeStatic + def alive(self): + """ Returns True. Useful for PED-RPC clients who want to see if the PED-RPC connection is still alive. - ''' + """ return True - def log (self, msg="", level=1): - ''' + def log(self, msg="", level=1): + """ If the supplied message falls under the current log level, print the specified message to screen. - @type msg: String + @type msg: str @param msg: Message to log - ''' + """ if self.log_level >= level: print "[%s] %s" % (time.strftime("%I:%M.%S"), msg) - def post_send (self): - ''' + def post_send(self): + """ This routine is called after the fuzzer transmits a test case and returns the status of the target. - @rtype: Boolean + @rtype: bool @return: Return True if the target is still active, False otherwise. - ''' + """ - if not self.dbg.isAlive(): + if not self.dbg.is_alive(): exit_status = self.dbg.get_exit_status() rec_file = open(self.crash_bin, 'a') if os.WCOREDUMP(exit_status): @@ -150,45 +154,49 @@ def post_send (self): reason = 'Exit with code - ' + str(os.WEXITSTATUS(exit_status)) else: reason = 'Process died for unknown reason' - - self.last_synopsis = '[%s] Crash : Test - %d Reason - %s\n' % (time.strftime("%I:%M.%S"), self.test_number, reason) + + self.last_synopsis = '[%s] Crash : Test - %d Reason - %s\n' % ( + time.strftime("%I:%M.%S"), + self.test_number, + reason + ) rec_file.write(self.last_synopsis) rec_file.close() - return self.dbg.isAlive() + return self.dbg.is_alive() - def pre_send (self, test_number): - ''' + def pre_send(self, test_number): + """ This routine is called before the fuzzer transmits a test case and ensure the debugger thread is operational. (In this implementation do nothing for now) @type test_number: Integer @param test_number: Test number to retrieve PCAP for. - ''' - if self.dbg == None: + """ + if not self.dbg: self.start_target() - + self.log("pre_send(%d)" % test_number, 10) self.test_number = test_number - def start_target (self): - ''' + def start_target(self): + """ Start up the target process by issuing the commands in self.start_commands. - ''' + """ self.log("starting target process") - - self.dbg = debugger_thread(self.start_commands[0]) + + self.dbg = DebuggerThread(self.start_commands[0]) self.dbg.spawn_target() # prevent blocking by spawning off another thread to waitpid threading.Thread(target=self.dbg.start_monitoring).start() self.log("done. target up and running, giving it 5 seconds to settle in.") time.sleep(5) - def stop_target (self): - ''' + def stop_target(self): + """ Kill the current debugger thread and stop the target process by issuing the commands in self.stop_commands. - ''' + """ # give the debugger thread a chance to exit. time.sleep(1) @@ -201,11 +209,11 @@ def stop_target (self): else: os.system(command) - def set_start_commands (self, start_commands): - ''' + def set_start_commands(self, start_commands): + """ We expect start_commands to be a list with one element for example ['/usr/bin/program arg1 arg2 arg3'] - ''' + """ if len(start_commands) > 1: self.log("This process monitor does not accept > 1 start command") @@ -214,33 +222,32 @@ def set_start_commands (self, start_commands): self.log("updating start commands to: %s" % start_commands) self.start_commands = start_commands - - def set_stop_commands (self, stop_commands): + def set_stop_commands(self, stop_commands): self.log("updating stop commands to: %s" % stop_commands) self.stop_commands = stop_commands - def set_proc_name (self, proc_name): + def set_proc_name(self, proc_name): self.log("updating target process name to '%s'" % proc_name) self.proc_name = proc_name - def get_crash_synopsis (self): - ''' + def get_crash_synopsis(self): + """ Return the last recorded crash synopsis. @rtype: String @return: Synopsis of last recorded crash. - ''' + """ return self.last_synopsis - -######################################################################################################################## + if __name__ == "__main__": # parse command line options. + opts = None try: - opts, args = getopt.getopt(sys.argv[1:], "c:P:l:", ["crash_bin=","port=","log_level=",]) + opts, args = getopt.getopt(sys.argv[1:], "c:P:l:", ["crash_bin=", "port=", "log_level="]) except getopt.GetoptError: ERR(USAGE) @@ -248,17 +255,21 @@ def get_crash_synopsis (self): PORT = None crash_bin = None for opt, arg in opts: - if opt in ("-c", "--crash_bin"): crash_bin = arg - if opt in ("-P", "--port"): PORT = int(arg) - if opt in ("-l", "--log_level"): log_level = int(arg) + if opt in ("-c", "--crash_bin"): + crash_bin = arg + if opt in ("-P", "--port"): + PORT = int(arg) + if opt in ("-l", "--log_level"): + log_level = int(arg) + + if not crash_bin: + ERR(USAGE) - if not crash_bin: ERR(USAGE) - - if PORT == None: + if not PORT: PORT = 26002 - + # spawn the PED-RPC servlet. - servlet = nix_process_monitor_pedrpc_server("0.0.0.0", PORT, crash_bin, log_level) + servlet = NIXProcessMonitorPedrpcServer("0.0.0.0", PORT, crash_bin, log_level) servlet.serve_forever() diff --git a/requests/___REQUESTS___.html b/requests/___REQUESTS___.html index cbe8f2c..fab80d6 100644 --- a/requests/___REQUESTS___.html +++ b/requests/___REQUESTS___.html @@ -53,7 +53,7 @@

Trend Micro

20901

This fuzz request targets Trend Micro Control Manager (DcsProcessor.exe) TCP port 20901. For more information about the audit target see: - + The CRC field in this protocol is calculated in some non-standard fashion so the check must be NOP-ed out in the target binary. The protocol is also obfuscated using a basic XOR routine. I wrote an appropriate block encoder named trend_xor_encode(), a decoder is defined as well. This fuzz found nothing so far! I need to uncover more protocol details. See also: Pedram's pwned notebook page 3, 4. diff --git a/requests/hp.py b/requests/hp.py index f78844f..cd5b8ce 100644 --- a/requests/hp.py +++ b/requests/hp.py @@ -3,7 +3,6 @@ from struct import * -######################################################################################################################## def unicode_ftw(val): """ Simple unicode slicer @@ -20,13 +19,12 @@ def unicode_ftw(val): return ret -######################################################################################################################## + s_initialize("omni") """ Hewlett Packard OpenView Data Protector OmniInet.exe """ - if s_block_start("packet_1"): s_size("packet_2", endian=">", length=4) s_block_end() @@ -38,7 +36,7 @@ def unicode_ftw(val): # unicode magic if s_block_start("unicode_magic", encoder=unicode_ftw): - s_int(267, format="ascii") + s_int(267, output_format="ascii") s_block_end() s_static("\x00\x00") @@ -47,7 +45,7 @@ def unicode_ftw(val): # unicode value to pass calls to wtoi() if s_block_start("unicode_100_1", encoder=unicode_ftw): - s_int(100, format="ascii") + s_int(100, output_format="ascii") s_block_end() s_static("\x00\x00") @@ -56,7 +54,7 @@ def unicode_ftw(val): # unicode value to pass calls to wtoi() if s_block_start("unicode_100_2", encoder=unicode_ftw): - s_int(100, format="ascii") + s_int(100, output_format="ascii") s_block_end() s_static("\x00\x00") @@ -65,12 +63,12 @@ def unicode_ftw(val): # unicode value to pass calls to wtoi() if s_block_start("unicode_100_3", encoder=unicode_ftw): - s_int(100, format="ascii") + s_int(100, output_format="ascii") s_block_end() s_static("\x00\x00") # random buffer - s_string("D"*32, size=32) + s_string("D" * 32, size=32) # barhost cookie s_dword(0x7cde7bab, endian="<", fuzzable=False) diff --git a/requests/http.py b/requests/http.py index 7f0d74b..61cf31d 100644 --- a/requests/http.py +++ b/requests/http.py @@ -1,7 +1,7 @@ from sulley import * -######################################################################################################################## + # Old http.py request primitives, http_* does all of these and many more (AFAIK) -######################################################################################################################## + # List of all blocks defined here (for easy copy/paste) """ sess.connect(s_get("HTTP VERBS")) @@ -26,8 +26,6 @@ s_static("\r\n\r\n") s_block_end() - -######################################################################################################################## s_initialize("HTTP VERBS BASIC") s_group("verbs", values=["GET", "HEAD"]) if s_block_start("body", group="verbs"): @@ -43,23 +41,19 @@ s_static("\r\n\r\n") s_block_end() - -######################################################################################################################## s_initialize("HTTP VERBS POST") s_static("POST / HTTP/1.1\r\n") s_static("Content-Type: ") s_string("application/x-www-form-urlencoded") s_static("\r\n") s_static("Content-Length: ") -s_size("post blob", format="ascii", signed=True, fuzzable=True) +s_size("post blob", output_format="ascii", signed=True, fuzzable=True) s_static("\r\n\r\n") if s_block_start("post blob"): - s_string("A"*100 + "=" + "B"*100) + s_string("A" * 100 + "=" + "B" * 100) s_block_end() - -######################################################################################################################## s_initialize("HTTP HEADERS") s_static("GET / HTTP/1.1\r\n") @@ -94,8 +88,6 @@ s_static("\r\n") s_static("\r\n") - -######################################################################################################################## s_initialize("HTTP COOKIE") s_static("GET / HTTP/1.1\r\n") diff --git a/requests/http_get.py b/requests/http_get.py index af4b762..3b13a62 100644 --- a/requests/http_get.py +++ b/requests/http_get.py @@ -1,7 +1,7 @@ from sulley import * -######################################################################################################################## + # All HTTP requests that I could think of/find -######################################################################################################################## + # List of all blocks defined here (for easy copy/paste) """ sess.connect(s_get("HTTP VERBS")) @@ -9,13 +9,14 @@ sess.connect(s_get("HTTP REQ")) """ -######################################################################################################################## + # Fuzz all the publicly avalible methods known for HTTP Servers -######################################################################################################################## + s_initialize("HTTP VERBS") -s_group("verbs", values=["GET", "HEAD", "POST", "OPTIONS", "TRACE", "PUT", "DELETE", "PROPFIND","CONNECT","PROPPATCH", - "MKCOL","COPY","MOVE","LOCK","UNLOCK","VERSION-CONTROL","REPORT","CHECKOUT","CHECKIN","UNCHECKOUT", - "MKWORKSPACE","UPDATE","LABEL","MERGE","BASELINE-CONTROL","MKACTIVITY","ORDERPATCH","ACL","PATCH","SEARCH","CAT"]) +s_group("verbs", values=["GET", "HEAD", "POST", "OPTIONS", "TRACE", "PUT", "DELETE", "PROPFIND", "CONNECT", "PROPPATCH", + "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", + "UNCHECKOUT", "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", + "ORDERPATCH", "ACL", "PATCH", "SEARCH", "CAT"]) if s_block_start("body", group="verbs"): s_delim(" ") s_delim("/") @@ -23,21 +24,21 @@ s_delim(" ") s_string("HTTP") s_delim("/") - s_int(1,format="ascii") + s_int(1, output_format="ascii") s_delim(".") - s_int(1,format="ascii") + s_int(1, output_format="ascii") s_static("\r\n\r\n") s_block_end() -######################################################################################################################## + # Fuzz the HTTP Method itself -######################################################################################################################## + s_initialize("HTTP METHOD") s_string("FUZZ") s_static(" /index.html HTTP/1.1") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz this standard multi-header HTTP request # GET / HTTP/1.1 # Host: www.google.com @@ -47,7 +48,7 @@ # Accept-Encoding: gzip,deflate,sdch # Accept-Language: en-US,en;q=0.8 # Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3 -######################################################################################################################## + s_initialize("HTTP REQ") s_static("GET / HTTP/1.1\r\n") # Host: www.google.com @@ -87,9 +88,9 @@ s_delim(";") s_string("q") s_delim("=") -s_int(0,format="ascii") +s_int(0, output_format="ascii") s_delim(".") -s_int(9,format="ascii") +s_int(9, output_format="ascii") s_delim(",") s_string("*") s_delim("/") @@ -97,9 +98,9 @@ s_delim(";") s_string("q") s_delim("=") -s_int(0,format="ascii") +s_int(0, output_format="ascii") s_delim(".") -s_int(8,format="ascii") +s_int(8, output_format="ascii") s_static("\r\n") # Accept-Encoding: gzip,deflate,sdch s_static("Accept-Encoding") @@ -129,23 +130,23 @@ s_delim(" ") s_string("ISO") s_delim("-") -s_int(8859,format="ascii") +s_int(8859, output_format="ascii") s_delim("-") -s_int(1,format="ascii") +s_int(1, output_format="ascii") s_delim(",") s_string("utf-8") s_delim(";") s_string("q") s_delim("=") -s_int(0,format="ascii") +s_int(0, output_format="ascii") s_delim(".") -s_int(7,format="ascii") +s_int(7, output_format="ascii") s_delim(",") s_string("*") s_delim(";") s_string("q") s_delim("=") -s_int(0,format="ascii") +s_int(0, output_format="ascii") s_delim(".") -s_int(3,format="ascii") +s_int(3, output_format="ascii") s_static("\r\n\r\n") diff --git a/requests/http_header.py b/requests/http_header.py index c99ab8d..91e1a58 100644 --- a/requests/http_header.py +++ b/requests/http_header.py @@ -1,7 +1,7 @@ from sulley import * -######################################################################################################################## + # List of all HTTP Headers I could find -######################################################################################################################## + # List of all blocks defined here (for easy copy/paste) """ sess.connect(s_get("HTTP HEADER ACCEPT")) @@ -43,10 +43,10 @@ sess.connect(s_get("HTTP HEADER XWAPPROFILE")) """ -######################################################################################################################## + # Fuzz Accept header # Accept: text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5 -######################################################################################################################## + s_initialize("HTTP HEADER ACCEPT") s_static("GET / HTTP/1.1\r\n") s_static("Accept") @@ -58,9 +58,9 @@ s_delim(";") s_string("q") s_delim("=") -s_int(0,format="ascii") +s_int(0, output_format="ascii") s_delim(".") -s_int(3,format="ascii") +s_int(3, output_format="ascii") s_delim(",") s_delim(" ") s_string("text") @@ -69,9 +69,9 @@ s_delim(";") s_string("q") s_delim("=") -s_int(0,format="ascii") +s_int(0, output_format="ascii") s_delim(".") -s_int(7,format="ascii") +s_int(7, output_format="ascii") s_delim(",") s_delim(" ") s_string("text") @@ -89,13 +89,13 @@ s_delim(";") s_string("level") s_delim("=") -s_int(2,format="ascii") +s_int(2, output_format="ascii") s_delim(";") s_string("q") s_delim("=") -s_int(0,format="ascii") +s_int(0, output_format="ascii") s_delim(".") -s_int(4,format="ascii") +s_int(4, output_format="ascii") s_delim(",") s_delim(" ") s_string("*") @@ -104,15 +104,15 @@ s_delim(";") s_string("q") s_delim("=") -s_int(0,format="ascii") +s_int(0, output_format="ascii") s_delim(".") -s_int(5,format="ascii") +s_int(5, output_format="ascii") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Accept-Charset header # Accept-Charset: utf-8, unicode-1-1;q=0.8 -######################################################################################################################## + s_initialize("HTTP HEADER ACCEPTCHARSET") s_static("GET / HTTP/1.1\r\n") s_static("Accept-Charset") @@ -120,26 +120,26 @@ s_delim(" ") s_string("utf") s_delim("-") -s_int(8,format="ascii") +s_int(8, output_format="ascii") s_delim(",") s_delim(" ") s_string("unicode") s_delim("-") -s_int(1,format="ascii") +s_int(1, output_format="ascii") s_delim("-") -s_int(1,format="ascii") +s_int(1, output_format="ascii") s_delim(";") s_string("q") s_delim("=") -s_int(0,format="ascii") +s_int(0, output_format="ascii") s_delim(".") -s_int(8,format="ascii") +s_int(8, output_format="ascii") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Accept-Datetime header # Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT -######################################################################################################################## + s_initialize("HTTP HEADER ACCEPTDATETIME") s_static("GET / HTTP/1.1\r\n") s_static("Accept-Datetime") @@ -163,10 +163,10 @@ s_string("GMT") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Accept-Encoding header # Accept-Encoding: gzip, deflate -######################################################################################################################## + s_initialize("HTTP HEADER ACCEPTENCODING") s_static("GET / HTTP/1.1\r\n") s_static("Accept-Encoding") @@ -177,10 +177,10 @@ s_string("deflate") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Accept-Language header # Accept-Language: en-us, en;q=0.5 -######################################################################################################################## + s_initialize("HTTP HEADER ACCEPTLANGUAGE") s_static("GET / HTTP/1.1\r\n") s_static("Accept-Language") @@ -196,10 +196,8 @@ s_static("\r\n\r\n") -######################################################################################################################## # Fuzz Authorization header # Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== -######################################################################################################################## s_initialize("HTTP HEADER AUTHORIZATION") s_static("GET / HTTP/1.1\r\n") s_static("Authorization") @@ -210,10 +208,9 @@ s_string("QWxhZGRpbjpvcGVuIHNlc2FtZQ==") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Cache-Control header # Cache-Control: no-cache -######################################################################################################################## s_initialize("HTTP HEADER CACHECONTROL") s_static("GET / HTTP/1.1\r\n") s_static("Cache-Control") @@ -224,10 +221,9 @@ s_string("cache") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Connection header # Connection: close -######################################################################################################################## s_initialize("HTTP HEADER CLOSE") s_static("GET / HTTP/1.1\r\n") s_static("Connection") @@ -236,10 +232,9 @@ s_string("close") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Content Length header # Content-Length: 348 -######################################################################################################################## s_initialize("HTTP HEADER CONTENTLENGTH") s_static("GET / HTTP/1.1\r\n") s_static("Content-Length") @@ -248,10 +243,9 @@ s_string("348") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Content MD5 header # Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ== -######################################################################################################################## s_initialize("HTTP HEADER CONTENTMD5") s_static("GET / HTTP/1.1\r\n") s_static("Content-MD5") @@ -260,10 +254,9 @@ s_string("Q2hlY2sgSW50ZWdyaXR5IQ==") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz COOKIE header # Cookie: PHPSESSIONID=hLKQPySBvyTRq5K5RJmcTHQVtQycmwZG3Qvr0tSy2w9mQGmbJbJn; -######################################################################################################################## s_initialize("HTTP HEADER COOKIE") s_static("GET / HTTP/1.1\r\n") @@ -281,10 +274,9 @@ s_repeat("cookie", max_reps=5000, step=500) s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Date header # Date: Tue, 15 Nov 2012 08:12:31 EST -######################################################################################################################## s_initialize("HTTP HEADER DATE") s_static("GET / HTTP/1.1\r\n") s_static("Date") @@ -308,10 +300,9 @@ s_string("EST") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz DNT header -> May be same as X-Do-Not-Track? # DNT: 1 -######################################################################################################################## s_initialize("HTTP HEADER DNT") s_static("GET / HTTP/1.1\r\n") s_static("DNT") @@ -320,10 +311,9 @@ s_string("1") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Expect header # Expect: 100-continue -######################################################################################################################## s_initialize("HTTP HEADER EXPECT") s_static("GET / HTTP/1.1\r\n") s_static("Expect") @@ -334,10 +324,9 @@ s_string("continue") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz From header # From: derp@derp.com -######################################################################################################################## s_initialize("HTTP HEADER FROM") s_static("GET / HTTP/1.1\r\n") s_static("From") @@ -350,10 +339,9 @@ s_string("com") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Host header # Host: 127.0.0.1 -######################################################################################################################## s_initialize("HTTP HEADER HOST") s_static("GET / HTTP/1.1\r\n") s_static("Host") @@ -368,10 +356,8 @@ s_static("\r\n\r\n") -######################################################################################################################## # Fuzz If-Match header # If-Match: "737060cd8c284d8af7ad3082f209582d" -######################################################################################################################## s_initialize("HTTP HEADER IFMATCH") s_static("GET / HTTP/1.1\r\n") s_static("If-Match") @@ -382,10 +368,9 @@ s_static("\"") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz If-Modified-Since header # If-Modified-Since: Sat, 29 Oct 2012 19:43:31 ESTc -######################################################################################################################## s_initialize("HTTP HEADER IFMODIFIEDSINCE") s_static("GET / HTTP/1.1\r\n") s_static("If-Modified-Since") @@ -409,10 +394,9 @@ s_string("EST") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz If-None-Match header # If-None-Match: "737060cd8c284d8af7ad3082f209582d" -######################################################################################################################## s_initialize("HTTP HEADER IFNONEMATCH") s_static("GET / HTTP/1.1\r\n") s_static("If-None-Match") @@ -423,10 +407,9 @@ s_static("\"") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz If-Range header # If-Range: "737060cd8c284d8af7ad3082f209582d" -######################################################################################################################## s_initialize("HTTP HEADER IFRANGE") s_static("GET / HTTP/1.1\r\n") s_static("If-Range") @@ -437,10 +420,9 @@ s_static("\"") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz If-Unmodified-Since header # If-Unmodified-Since: Sat, 29 Oct 2012 19:43:31 EST -######################################################################################################################## s_initialize("HTTP HEADER IFUNMODIFIEDSINCE") s_static("GET / HTTP/1.1\r\n") s_static("If-Unmodified-Since") @@ -464,10 +446,9 @@ s_string("EST") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz KeepAlive header # Keep-Alive: 300 -######################################################################################################################## s_initialize("HTTP HEADER KEEPALIVE") s_static("GET / HTTP/1.1\r\n") s_static("Keep-Alive") @@ -476,10 +457,9 @@ s_string("300") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Max-Fowards header # Max-Forwards: 80 -######################################################################################################################## s_initialize("HTTP HEADER MAXFORWARDS") s_static("GET / HTTP/1.1\r\n") s_static("Max-Forwards") @@ -488,10 +468,9 @@ s_string("80") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Pragma header # Pragma: no-cache -######################################################################################################################## s_initialize("HTTP HEADER PRAGMA") s_static("GET / HTTP/1.1\r\n") s_static("Pragma") @@ -500,10 +479,9 @@ s_string("no-cache") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Proxy-Authorization header # Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== -######################################################################################################################## s_initialize("HTTP HEADER PROXYAUTHORIZATION") s_static("GET / HTTP/1.1\r\n") s_static("Proxy-Authorization") @@ -514,10 +492,9 @@ s_string("QWxhZGRpbjpvcGVuIHNlc2FtZQ==") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Range header # Range: bytes=500-999 -######################################################################################################################## s_initialize("HTTP HEADER RANGE") s_static("GET / HTTP/1.1\r\n") s_static("Range") @@ -530,10 +507,9 @@ s_string("999") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Referer header # Referer: http://www.google.com -######################################################################################################################## s_initialize("HTTP HEADER REFERER") s_static("GET / HTTP/1.1\r\n") s_static("Referer") @@ -542,10 +518,9 @@ s_string("http://www.google.com") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz TE header # TE: trailers, deflate -######################################################################################################################## s_initialize("HTTP HEADER TE") s_static("GET / HTTP/1.1\r\n") s_static("TE") @@ -557,10 +532,9 @@ s_string("deflate") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Upgrade header # Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 -######################################################################################################################## s_initialize("HTTP HEADER UPGRADE") s_static("GET / HTTP/1.1\r\n") s_static("Upgrade") @@ -592,10 +566,9 @@ s_string("x11") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz User Agent header # User-Agent: Mozilla/5.0 (Windows; U) -######################################################################################################################## s_initialize("HTTP HEADER USERAGENT") s_static("GET / HTTP/1.1\r\n") s_static("User-Agent") @@ -604,10 +577,9 @@ s_string("Mozilla/5.0 (Windows; U)") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Via header # Via: 1.0 derp, 1.1 derp.com (Apache/1.1) -######################################################################################################################## s_initialize("HTTP HEADER VIA") s_static("GET / HTTP/1.1\r\n") s_static("Via") @@ -635,10 +607,9 @@ s_delim(")") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz Warning header # Warning: 4141 Sulley Rocks! -######################################################################################################################## s_initialize("HTTP HEADER WARNING") s_static("GET / HTTP/1.1\r\n") s_static("Warning") @@ -649,10 +620,9 @@ s_string("Sulley Rocks!") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz X-att-deviceid header # x-att-deviceid: DerpPhone/Rev2309 -######################################################################################################################## s_initialize("HTTP HEADER XATTDEVICEID") s_static("GET / HTTP/1.1\r\n") s_static("x-att-deviceid") @@ -663,10 +633,9 @@ s_string("Rev2309") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz X-Do-Not-Track header # X-Do-Not-Track: 1 -######################################################################################################################## s_initialize("HTTP HEADER XDONOTTRACK") s_static("GET / HTTP/1.1\r\n") s_static("X-Do-Not-Track") @@ -675,10 +644,9 @@ s_string("1") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz X-Forwarded-For header # X-Forwarded-For: client1, proxy1, proxy2 -######################################################################################################################## s_initialize("HTTP HEADER XFORWARDEDFOR") s_static("GET / HTTP/1.1\r\n") s_static("X-Forwarded-For") @@ -690,10 +658,9 @@ s_string("proxy2") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz X-Requested-With header # X-Requested-With: XMLHttpRequest -######################################################################################################################## s_initialize("HTTP HEADER XREQUESTEDWITH") s_static("GET / HTTP/1.1\r\n") s_static("X-Requested-With") @@ -702,10 +669,9 @@ s_string("XMLHttpRequest") s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz X-WAP-Profile header # x-wap-profile: http://wap.samsungmobile.com/uaprof/SGH-I777.xml -######################################################################################################################## s_initialize("HTTP HEADER XWAPPROFILE") s_static("GET / HTTP/1.1\r\n") s_static("x-wap-profile") diff --git a/requests/http_post.py b/requests/http_post.py index 70b3209..52c8c8a 100644 --- a/requests/http_post.py +++ b/requests/http_post.py @@ -1,7 +1,7 @@ from sulley import * -######################################################################################################################## + # All POST mimetypes that I could think of/find -######################################################################################################################## + # List of all blocks defined here (for easy copy/paste) """ sess.connect(s_get("HTTP VERBS POST")) @@ -9,51 +9,84 @@ sess.connect(s_get("HTTP VERBS POST REQ")) """ -######################################################################################################################## + # Fuzz POST requests with most MIMETypes known -######################################################################################################################## + s_initialize("HTTP VERBS POST ALL") s_static("POST / HTTP/1.1\r\n") s_static("Content-Type: ") -s_group("mimetypes",values=["audio/basic","audio/x-mpeg","drawing/x-dwf","graphics/x-inventor","image/x-portable-bitmap", - "message/external-body","message/http","message/news","message/partial","message/rfc822", - "multipart/alternative","multipart/appledouble","multipart/digest","multipart/form-data", - "multipart/header-set","multipart/mixed","multipart/parallel","multipart/related","multipart/report", - "multipart/voice-message","multipart/x-mixed-replace","text/css","text/enriched","text/html", - "text/javascript","text/plain","text/richtext","text/sgml","text/tab-separated-values","text/vbscript", - "video/x-msvideo","video/x-sgi-movie","workbook/formulaone","x-conference/x-cooltalk","x-form/x-openscape", - "x-music/x-midi","x-script/x-wfxclient","x-world/x-3dmf"]) +s_group("mimetypes", values=[ + "audio/basic", + "audio/x-mpeg", + "drawing/x-dwf", + "graphics/x-inventor", + "image/x-portable-bitmap", + "message/external-body", + "message/http", + "message/news", + "message/partial", + "message/rfc822", + "multipart/alternative", + "multipart/appledouble", + "multipart/digest", + "multipart/form-data", + "multipart/header-set", + "multipart/mixed", + "multipart/parallel", + "multipart/related", + "multipart/report", + "multipart/voice-message", + "multipart/x-mixed-replace", + "text/css", + "text/enriched", + "text/html", + "text/javascript", + "text/plain", + "text/richtext", + "text/sgml", + "text/tab-separated-values", + "text/vbscript", + "video/x-msvideo", + "video/x-sgi-movie", + "workbook/formulaone", + "x-conference/x-cooltalk", + "x-form/x-openscape", + "x-music/x-midi", + "x-script/x-wfxclient", + "x-world/x-3dmf" +]) + if s_block_start("mime", group="mimetypes"): s_static("\r\n") s_static("Content-Length: ") - s_size("post blob", format="ascii", signed=True, fuzzable=True) + s_size("post blob", output_format="ascii", signed=True, fuzzable=True) s_static("\r\n\r\n") s_block_end() if s_block_start("post blob"): - s_string("A"*100 + "=" + "B"*100) + s_string("A" * 100 + "=" + "B" * 100) s_block_end() s_static("\r\n\r\n") -######################################################################################################################## + # Basic fuzz of post payloads -######################################################################################################################## + s_initialize("HTTP VERBS POST") s_static("POST / HTTP/1.1\r\n") s_static("Content-Type: ") s_string("application/x-www-form-urlencoded") s_static("\r\n") s_static("Content-Length: ") -s_size("post blob", format="ascii", signed=True, fuzzable=True) +s_size("post blob", output_format="ascii", signed=True, fuzzable=True) s_static("\r\n") if s_block_start("post blob"): - s_string("A"*100 + "=" + "B"*100) + s_string("A" * 100 + "=" + "B" * 100) s_block_end() s_static("\r\n\r\n") -######################################################################################################################## + # Fuzz POST request MIMETypes -######################################################################################################################## + s_initialize("HTTP VERBS POST REQ") s_static("POST / HTTP/1.1\r\n") s_static("Content-Type: ") @@ -68,9 +101,9 @@ s_string("urlencoded") s_static("\r\n") s_static("Content-Length: ") -s_size("post blob", format="ascii", signed=True, fuzzable=True) +s_size("post blob", output_format="ascii", signed=True, fuzzable=True) s_static("\r\n") if s_block_start("post blob"): - s_string("A"*100 + "=" + "B"*100) + s_string("A" * 100 + "=" + "B" * 100) s_block_end() s_static("\r\n\r\n") \ No newline at end of file diff --git a/requests/jabber.py b/requests/jabber.py index 2196e12..5f9d192 100644 --- a/requests/jabber.py +++ b/requests/jabber.py @@ -1,7 +1,6 @@ from sulley import * -######################################################################################################################## s_initialize("chat init") """ @@ -14,8 +13,6 @@ s_static('') s_static('') - -######################################################################################################################## s_initialize("chat message") s_static('\n') s_static('\n') @@ -43,14 +40,21 @@ s_string("hello from python!") s_static("\n") -# s_static('hello from python\n') +# s_static(' +# +# +# hello from python +# +# +# \n +# ') s_static('') s_static("<") s_string("font") s_static(' face="') s_string("Helvetica") s_string('" ABSZ="') -s_word(12, format="ascii", signed=True) +s_word(12, output_format="ascii", signed=True) s_static('" color="') s_string("#000000") s_static('">') diff --git a/requests/ldap.py b/requests/ldap.py index 350e685..0f033ff 100644 --- a/requests/ldap.py +++ b/requests/ldap.py @@ -20,7 +20,6 @@ 15 AbandonRequest """ -######################################################################################################################## s_initialize("anonymous bind") # all ldap messages start with this. @@ -31,22 +30,20 @@ s_sizer("envelope", endian=">") if s_block_start("envelope"): - s_static("\x02\x01\x01") # message id (always one) - s_static("\x60") # bind request (0) + s_static("\x02\x01\x01") # message id (always one) + s_static("\x60") # bind request (0) s_static("\x84") s_sizer("bind request", endian=">") if s_block_start("bind request"): - s_static("\x02\x01\x03") # version + s_static("\x02\x01\x03") # version s_lego("ber_string", "anonymous") - s_lego("ber_string", "foobar", options={"prefix":"\x80"}) # 0x80 is "simple" authentication + s_lego("ber_string", "foobar", options={"prefix": "\x80"}) # 0x80 is "simple" authentication s_block_end() s_block_end() - -######################################################################################################################## s_initialize("search request") # all ldap messages start with this. @@ -57,20 +54,20 @@ s_sizer("envelope", endian=">", fuzzable=True) if s_block_start("envelope"): - s_static("\x02\x01\x02") # message id (always one) - s_static("\x63") # search request (3) + s_static("\x02\x01\x02") # message id (always one) + s_static("\x63") # search request (3) s_static("\x84") s_sizer("searchRequest", endian=">", fuzzable=True) if s_block_start("searchRequest"): - s_static("\x04\x00") # static empty string ... why? - s_static("\x0a\x01\x00") # scope: baseOjbect (0) - s_static("\x0a\x01\x00") # deref: never (0) - s_lego("ber_integer", 1000) # size limit - s_lego("ber_integer", 30) # time limit - s_static("\x01\x01\x00") # typesonly: false - s_lego("ber_string", "objectClass", options={"prefix":"\x87"}) + s_static("\x04\x00") # static empty string ... why? + s_static("\x0a\x01\x00") # scope: baseOjbect (0) + s_static("\x0a\x01\x00") # deref: never (0) + s_lego("ber_integer", 1000) # size limit + s_lego("ber_integer", 30) # time limit + s_static("\x01\x01\x00") # typesonly: false + s_lego("ber_string", "objectClass", options={"prefix": "\x87"}) s_static("\x30") s_static("\x84") diff --git a/requests/mcafee.py b/requests/mcafee.py index de5eb91..eb4e4bc 100644 --- a/requests/mcafee.py +++ b/requests/mcafee.py @@ -2,9 +2,9 @@ from struct import * + # stupid one byte XOR -def mcafee_epo_xor (buf, poly=0xAA): - l = len(buf) +def mcafee_epo_xor(buf, poly=0xAA): new_buf = "" for char in buf: @@ -12,7 +12,7 @@ def mcafee_epo_xor (buf, poly=0xAA): return new_buf -######################################################################################################################## + s_initialize("mcafee_epo_framework_tcp") """ McAfee FrameworkService.exe TCP port 8081 @@ -29,7 +29,7 @@ def mcafee_epo_xor (buf, poly=0xAA): s_static("Content-Length:") s_delim(" ") -s_size("payload", format="ascii") +s_size("payload", output_format="ascii") s_delim("\r\n\r\n") if s_block_start("payload"): @@ -37,8 +37,6 @@ def mcafee_epo_xor (buf, poly=0xAA): s_delim("\r\n") s_block_end() - -######################################################################################################################## s_initialize("mcafee_epo_framework_udp") """ McAfee FrameworkService.exe UDP port 8082 @@ -46,12 +44,12 @@ def mcafee_epo_xor (buf, poly=0xAA): s_static('Type=\"AgentWakeup\"', name="agent_wakeup") s_static('\"DataSize=\"') -s_size("data", format="ascii") # must be over 234 +s_size("data", output_format="ascii") # must be over 234 if s_block_start("data", encoder=mcafee_epo_xor): s_static("\x50\x4f", name="signature") s_group(values=[pack('L", 1), name="sequence") + s_static(struct.pack(">L", 1), name="sequence") s_static(struct.pack(">L", time.time()), name="timestamp") - s_static(struct.pack(">L", 0), name="message type") # request (0) - s_static(struct.pack(">L", 0x901), name="NDMP_CONECT_CLIENT_AUTH") - s_static(struct.pack(">L", 1), name="reply sequence") - s_static(struct.pack(">L", 0), name="error") + s_static(struct.pack(">L", 0), name="message type") # request (0) + s_static(struct.pack(">L", 0x901), name="NDMP_CONECT_CLIENT_AUTH") + s_static(struct.pack(">L", 1), name="reply sequence") + s_static(struct.pack(">L", 0), name="error") s_block_end("ndmp header") s_group("auth types", values=[struct.pack(">L", 190), struct.pack(">L", 5), struct.pack(">L", 4)]) @@ -110,12 +107,10 @@ s_random(0, min_length=1000, max_length=50000, num_mutations=500) # random valid XDR string. - #s_lego("xdr_string", "pedram") + # s_lego("xdr_string", "pedram") s_block_end("body") s_block_end("request") - -######################################################################################################################## s_initialize("Veritas Proprietary Message Types") # the first bit is the last frag flag, we'll always set it and truncate our size to 3 bytes. @@ -125,27 +120,26 @@ if s_block_start("request"): if s_block_start("ndmp header"): - s_static(struct.pack(">L", 1), name="sequence") + s_static(struct.pack(">L", 1), name="sequence") s_static(struct.pack(">L", time.time()), name="timestamp") - s_static(struct.pack(">L", 0), name="message type") # request (0) - - s_group("prop ops", values = \ - [ - struct.pack(">L", 0xf315), # file list? - struct.pack(">L", 0xf316), - struct.pack(">L", 0xf317), - struct.pack(">L", 0xf200), # - struct.pack(">L", 0xf201), - struct.pack(">L", 0xf202), - struct.pack(">L", 0xf31b), - struct.pack(">L", 0xf270), # send strings like NDMP_PROP_PEER_PROTOCOL_VERSION - struct.pack(">L", 0xf271), - struct.pack(">L", 0xf33b), - struct.pack(">L", 0xf33c), - ]) - - s_static(struct.pack(">L", 1), name="reply sequence") - s_static(struct.pack(">L", 0), name="error") + s_static(struct.pack(">L", 0), name="message type") # request (0) + + s_group("prop ops", values=[ + struct.pack(">L", 0xf315), # file list? + struct.pack(">L", 0xf316), + struct.pack(">L", 0xf317), + struct.pack(">L", 0xf200), # + struct.pack(">L", 0xf201), + struct.pack(">L", 0xf202), + struct.pack(">L", 0xf31b), + struct.pack(">L", 0xf270), # send strings like NDMP_PROP_PEER_PROTOCOL_VERSION + struct.pack(">L", 0xf271), + struct.pack(">L", 0xf33b), + struct.pack(">L", 0xf33c), + ]) + + s_static(struct.pack(">L", 1), name="reply sequence") + s_static(struct.pack(">L", 0), name="error") s_block_end("ndmp header") if s_block_start("body", group="prop ops"): diff --git a/requests/rendezvous.py b/requests/rendezvous.py index 859eddc..08271fc 100644 --- a/requests/rendezvous.py +++ b/requests/rendezvous.py @@ -1,108 +1,104 @@ from sulley import * -######################################################################################################################## + s_initialize("trillian 1") -s_static("\x00\x00") # transaction ID -s_static("\x00\x00") # flags (standard query) -s_word(1, endian=">") # number of questions -s_word(0, endian=">", fuzzable=False) # answer RRs -s_word(0, endian=">", fuzzable=False) # authority RRs -s_word(0, endian=">", fuzzable=False) # additional RRs +s_static("\x00\x00") # transaction ID +s_static("\x00\x00") # flags (standard query) +s_word(1, endian=">") # number of questions +s_word(0, endian=">", fuzzable=False) # answer RRs +s_word(0, endian=">", fuzzable=False) # authority RRs +s_word(0, endian=">", fuzzable=False) # additional RRs # queries s_lego("dns_hostname", "_presence._tcp.local") -s_word(0x000c, endian=">") # type = pointer -s_word(0x8001, endian=">") # class = flush - +s_word(0x000c, endian=">") # type = pointer +s_word(0x8001, endian=">") # class = flush -######################################################################################################################## s_initialize("trillian 2") if s_block_start("pamini.local"): if s_block_start("header"): - s_static("\x00\x00") # transaction ID - s_static("\x00\x00") # flags (standard query) - s_word(2, endian=">") # number of questions - s_word(0, endian=">", fuzzable=False) # answer RRs - s_word(2, endian=">", fuzzable=False) # authority RRs - s_word(0, endian=">", fuzzable=False) # additional RRs + s_static("\x00\x00") # transaction ID + s_static("\x00\x00") # flags (standard query) + s_word(2, endian=">") # number of questions + s_word(0, endian=">", fuzzable=False) # answer RRs + s_word(2, endian=">", fuzzable=False) # authority RRs + s_word(0, endian=">", fuzzable=False) # additional RRs s_block_end() # queries s_lego("dns_hostname", "pamini.local") - s_word(0x00ff, endian=">") # type = any - s_word(0x8001, endian=">") # class = flush + s_word(0x00ff, endian=">") # type = any + s_word(0x8001, endian=">") # class = flush s_block_end() s_lego("dns_hostname", "pedram@PAMINI._presence._tcp") -s_word(0x00ff, endian=">") # type = any -s_word(0x8001, endian=">") # class = flush +s_word(0x00ff, endian=">") # type = any +s_word(0x8001, endian=">") # class = flush # authoritative nameservers -s_static("\xc0") # offset specifier -s_size("header", length=1) # offset to pamini.local -s_static("\x00\x01") # type = A (host address) -s_static("\x00\x01") # class = in -s_static("\x00\x00\x00\xf0") # ttl 4 minutes -s_static("\x00\x04") # data length -s_static(chr(152) + chr(67) + chr(137) + chr(53)) # ip address - -s_static("\xc0") # offset specifier -s_size("pamini.local", length=1) # offset to pedram@PAMINI... -s_static("\x00\x21") # type = SRV (service location) -s_static("\x00\x01") # class = in -s_static("\x00\x00\x00\xf0") # ttl 4 minutes -s_static("\x00\x08") # data length -s_static("\x00\x00") # priority -s_static("\x00\x00") # weight -s_static("\x14\xb2") # port -s_static("\xc0") # offset specifier -s_size("header", length=1) # offset to pamini.local - - -######################################################################################################################## +s_static("\xc0") # offset specifier +s_size("header", length=1) # offset to pamini.local +s_static("\x00\x01") # type = A (host address) +s_static("\x00\x01") # class = in +s_static("\x00\x00\x00\xf0") # ttl 4 minutes +s_static("\x00\x04") # data length +s_static(chr(152) + chr(67) + chr(137) + chr(53)) # ip address + +s_static("\xc0") # offset specifier +s_size("pamini.local", length=1) # offset to pedram@PAMINI... +s_static("\x00\x21") # type = SRV (service location) +s_static("\x00\x01") # class = in +s_static("\x00\x00\x00\xf0") # ttl 4 minutes +s_static("\x00\x08") # data length +s_static("\x00\x00") # priority +s_static("\x00\x00") # weight +s_static("\x14\xb2") # port +s_static("\xc0") # offset specifier +s_size("header", length=1) # offset to pamini.local + s_initialize("trillian 3") if s_block_start("pamini.local"): if s_block_start("header"): - s_static("\x00\x00") # transaction ID - s_static("\x00\x00") # flags (standard query) - s_word(2, endian=">") # number of questions - s_word(0, endian=">", fuzzable=False) # answer RRs - s_word(2, endian=">", fuzzable=False) # authority RRs - s_word(0, endian=">", fuzzable=False) # additional RRs + s_static("\x00\x00") # transaction ID + s_static("\x00\x00") # flags (standard query) + s_word(2, endian=">") # number of questions + s_word(0, endian=">", fuzzable=False) # answer RRs + s_word(2, endian=">", fuzzable=False) # authority RRs + s_word(0, endian=">", fuzzable=False) # additional RRs s_block_end() # queries s_lego("dns_hostname", "pamini.local") - s_word(0x00ff, endian=">") # type = any - s_word(0x0001, endian=">") # class = in + s_word(0x00ff, endian=">") # type = any + s_word(0x0001, endian=">") # class = in s_block_end() s_lego("dns_hostname", "pedram@PAMINI._presence._tcp") -s_word(0x00ff, endian=">") # type = any -s_word(0x0001, endian=">") # class = in +s_word(0x00ff, endian=">") # type = any +s_word(0x0001, endian=">") # class = in # authoritative nameservers -s_static("\xc0") # offset specifier -s_size("header", length=1) # offset to pamini.local -s_static("\x00\x01") # type = A (host address) -s_static("\x00\x01") # class = in -s_static("\x00\x00\x00\xf0") # ttl 4 minutes -s_static("\x00\x04") # data length -s_static(chr(152) + chr(67) + chr(137) + chr(53)) # ip address - -s_static("\xc0") # offset specifier -s_size("pamini.local", length=1) # offset to pedram@PAMINI... -s_static("\x00\x21") # type = SRV (service location) -s_static("\x00\x01") # class = in -s_static("\x00\x00\x00\xf0") # ttl 4 minutes -s_static("\x00\x08") # data length -s_static("\x00\x00") # priority -s_static("\x00\x00") # weight -s_static("\x14\xb2") # port -s_static("\xc0") # offset specifier -s_size("header", length=1) # offset to pamini.local \ No newline at end of file +s_static("\xc0") # offset specifier +s_size("header", length=1) # offset to pamini.local +s_static("\x00\x01") # type = A (host address) +s_static("\x00\x01") # class = in +s_static("\x00\x00\x00\xf0") # ttl 4 minutes +s_static("\x00\x04") # data length +s_static(chr(152) + chr(67) + chr(137) + chr(53)) # ip address + +s_static("\xc0") # offset specifier +s_size("pamini.local", length=1) # offset to pedram@PAMINI... +s_static("\x00\x21") # type = SRV (service location) +s_static("\x00\x01") # class = in +s_static("\x00\x00\x00\xf0") # ttl 4 minutes +s_static("\x00\x08") # data length +s_static("\x00\x00") # priority +s_static("\x00\x00") # weight +s_static("\x14\xb2") # port +s_static("\xc0") # offset specifier +s_size("header", length=1) # offset to pamini.local \ No newline at end of file diff --git a/requests/stun.py b/requests/stun.py index 97951ad..363e711 100644 --- a/requests/stun.py +++ b/requests/stun.py @@ -6,7 +6,7 @@ from sulley import * -######################################################################################################################## + s_initialize("binding request") # message type 0x0001: binding request. @@ -39,16 +39,14 @@ # toss out some large strings when the lengths are anything but valid. if s_block_start("fuzz block 1", dep="attribute length", dep_value=4, dep_compare="!="): - s_static("A"*5000) + s_static("A" * 5000) s_block_end() # toss out some large strings when the lengths are anything but valid. if s_block_start("fuzz block 2", dep="message length", dep_value=8, dep_compare="!="): - s_static("B"*5000) + s_static("B" * 5000) s_block_end() - -######################################################################################################################## s_initialize("binding response") # message type 0x0101: binding response \ No newline at end of file diff --git a/requests/trend.py b/requests/trend.py index 0aef323..18df0fe 100644 --- a/requests/trend.py +++ b/requests/trend.py @@ -2,55 +2,55 @@ import struct + # crap ass trend xor "encryption" routine for control manager (20901) -def trend_xor_encode (str): - ''' +def trend_xor_encode(string): + """ Simple bidirectional XOR "encryption" routine used by this service. - ''' + """ key = 0xA8534344 ret = "" # pad to 4 byte boundary. - pad = 4 - (len(str) % 4) + pad = 4 - (len(string) % 4) if pad == 4: pad = 0 - str += "\x00" * pad + string += "\x00" * pad - while str: - dword = struct.unpack(" + # s_delim("<") s_string("s") s_delim(":") @@ -87,7 +91,7 @@ # 7 s_static("") - s_dword(7, format="ascii", signed=True) + s_dword(7, output_format="ascii", signed=True) s_static("") # (upnp:class = "object.container.album.musicAlbum") @@ -108,12 +112,12 @@ # 0 s_static("") - s_dword(0, format="ascii", signed=True) + s_dword(0, output_format="ascii", signed=True) s_static("") # 1000 s_static("") - s_dword(1000, format="ascii", signed=True) + s_dword(1000, output_format="ascii", signed=True) s_static("") s_static("+dc:title") diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1161a97 --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + + +setup( + name='Sulley', + download_url='https://github.com/OpenRCE/sulley', + packages=['requests', 'sulley', 'sulley.legos', 'sulley.pgraph', 'sulley.utils', + 'unit_tests', 'utils', 'web'], + package_dir={'requests': './requests', + 'sulley': './sulley', + 'sulley.legos': './sulley/legos', + 'sulley.pgraph': './sulley/pgraph', + 'sulley.utils': './sulley/utils', + 'unit_tests': './unit_tests', + 'utils': './utils', + 'web': './web' + }, + install_requires=['pydot2==1.0.33', 'tornado==4.0.2', 'Flask==0.10.1'] +) \ No newline at end of file diff --git a/sulley/__init__.py b/sulley/__init__.py index 87f96b9..e16a9df 100644 --- a/sulley/__init__.py +++ b/sulley/__init__.py @@ -1,22 +1,17 @@ -import sulley.blocks -import sulley.instrumentation -import sulley.legos -import sulley.pedrpc -import sulley.primitives -import sulley.sex -import sulley.sessions -import sulley.utils +import blocks +import instrumentation +import legos +import pedrpc +import primitives +import sex +import sessions +import utils +from constants import BIG_ENDIAN, LITTLE_ENDIAN -BIG_ENDIAN = ">" -LITTLE_ENDIAN = "<" - -######################################################################################################################## ### REQUEST MANAGEMENT -######################################################################################################################## - -def s_get (name=None): - ''' +def s_get(name=None): + """ Return the request with the specified name or the current request if name is not specified. Use this to switch from global function style request manipulation to direct object manipulation. Example:: @@ -25,12 +20,12 @@ def s_get (name=None): The selected request is also set as the default current. (ie: s_switch(name) is implied). - @type name: String + @type name: str @param name: (Optional, def=None) Name of request to return or current request if name is None. @rtype: blocks.request @return: The requested request. - ''' + """ if not name: return blocks.CURRENT @@ -38,82 +33,80 @@ def s_get (name=None): # ensure this gotten request is the new current. s_switch(name) - if not blocks.REQUESTS.has_key(name): + if not name in blocks.REQUESTS: raise sex.SullyRuntimeError("blocks.REQUESTS NOT FOUND: %s" % name) return blocks.REQUESTS[name] -def s_initialize (name): - ''' +def s_initialize(name): + """ Initialize a new block request. All blocks / primitives generated after this call apply to the named request. Use s_switch() to jump between factories. - @type name: String + @type name: str @param name: Name of request - ''' - - if blocks.REQUESTS.has_key(name): + """ + if name in blocks.REQUESTS: raise sex.SullyRuntimeError("blocks.REQUESTS ALREADY EXISTS: %s" % name) - blocks.REQUESTS[name] = blocks.request(name) - blocks.CURRENT = blocks.REQUESTS[name] + blocks.REQUESTS[name] = blocks.Request(name) + blocks.CURRENT = blocks.REQUESTS[name] -def s_mutate (): - ''' +def s_mutate(): + """ Mutate the current request and return False if mutations are exhausted, in which case the request has been reverted back to its normal form. - @rtype: Boolean + @rtype: bool @return: True on mutation success, False if mutations exhausted. - ''' - + """ return blocks.CURRENT.mutate() -def s_num_mutations (): - ''' +def s_num_mutations(): + """ Determine the number of repetitions we will be making. - @rtype: Integer + @rtype: int @return: Number of mutated forms this primitive can take. - ''' + """ return blocks.CURRENT.num_mutations() -def s_render (): - ''' +def s_render(): + """ Render out and return the entire contents of the current request. @rtype: Raw @return: Rendered contents - ''' + """ return blocks.CURRENT.render() -def s_switch (name): - ''' +def s_switch(name): + """ Change the currect request to the one specified by "name". - @type name: String + @type name: str @param name: Name of request - ''' + """ - if not blocks.REQUESTS.has_key(name): + if not name in blocks.REQUESTS: raise sex.SullyRuntimeError("blocks.REQUESTS NOT FOUND: %s" % name) blocks.CURRENT = blocks.REQUESTS[name] -######################################################################################################################## -### BLOCK MANAGEMENT -######################################################################################################################## +# ## BLOCK MANAGEMENT + -def s_block_start (name, group=None, encoder=None, dep=None, dep_value=None, dep_values=[], dep_compare="=="): - ''' +def s_block_start(name, group=None, encoder=None, dep=None, dep_value=None, dep_values=(), dep_compare="=="): + #TODO: Either convert this to a with() statement, or add a new one that is compatible. + """ Open a new block under the current request. This routine always returns True so you can make your fuzzer pretty with indenting:: @@ -122,222 +115,227 @@ def s_block_start (name, group=None, encoder=None, dep=None, dep_value=None, dep if s_block_start("body"): ... - @type name: String + @type name: str @param name: Name of block being opened - @type group: String + @type group: str @param group: (Optional, def=None) Name of group to associate this block with @type encoder: Function Pointer @param encoder: (Optional, def=None) Optional pointer to a function to pass rendered data to prior to return - @type dep: String + @type dep: str @param dep: (Optional, def=None) Optional primitive whose specific value this block is dependant on @type dep_value: Mixed @param dep_value: (Optional, def=None) Value that field "dep" must contain for block to be rendered @type dep_values: List of Mixed Types @param dep_values: (Optional, def=[]) Values that field "dep" may contain for block to be rendered - @type dep_compare: String + @type dep_compare: str @param dep_compare: (Optional, def="==") Comparison method to use on dependency (==, !=, >, >=, <, <=) - ''' + """ - block = blocks.block(name, blocks.CURRENT, group, encoder, dep, dep_value, dep_values, dep_compare) + block = blocks.Block(name, blocks.CURRENT, group, encoder, dep, dep_value, dep_values, dep_compare) blocks.CURRENT.push(block) return True -def s_block_end (name=None): - ''' +# noinspection PyUnusedLocal +def s_block_end(name=None): + """ Close the last opened block. Optionally specify the name of the block being closed (purely for aesthetic purposes). - @type name: String + @type name: str @param name: (Optional, def=None) Name of block to closed. - ''' - + """ blocks.CURRENT.pop() -def s_checksum (block_name, algorithm="crc32", length=0, endian="<", name=None): - ''' +def s_checksum(block_name, algorithm="crc32", length=0, endian=LITTLE_ENDIAN, name=None): + """ Create a checksum block bound to the block with the specified name. You *can not* create a checksum for any currently open blocks. - @type block_name: String + @type block_name: str @param block_name: Name of block to apply sizer to - @type algorithm: String + @type algorithm: str @param algorithm: (Optional, def=crc32) Checksum algorithm to use. (crc32, adler32, md5, sha1) - @type length: Integer + @type length: int @param length: (Optional, def=0) Length of checksum, specify 0 to auto-calculate @type endian: Character @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) - @type name: String + @type name: str @param name: Name of this checksum field - ''' + """ # you can't add a checksum for a block currently in the stack. if block_name in blocks.CURRENT.block_stack: raise sex.SullyRuntimeError("CAN N0T ADD A CHECKSUM FOR A BLOCK CURRENTLY IN THE STACK") - checksum = blocks.checksum(block_name, blocks.CURRENT, algorithm, length, endian, name) + checksum = blocks.Checksum(block_name, blocks.CURRENT, algorithm, length, endian, name) blocks.CURRENT.push(checksum) -def s_repeat (block_name, min_reps=0, max_reps=None, step=1, variable=None, fuzzable=True, name=None): - ''' +def s_repeat(block_name, min_reps=0, max_reps=None, step=1, variable=None, fuzzable=True, name=None): + """ Repeat the rendered contents of the specified block cycling from min_reps to max_reps counting by step. By default renders to nothing. This block modifier is useful for fuzzing overflows in table entries. This block modifier MUST come after the block it is being applied to. @see: Aliases: s_repeater() - @type block_name: String + @type block_name: str @param block_name: Name of block to apply sizer to - @type min_reps: Integer + @type min_reps: int @param min_reps: (Optional, def=0) Minimum number of block repetitions - @type max_reps: Integer + @type max_reps: int @param max_reps: (Optional, def=None) Maximum number of block repetitions - @type step: Integer + @type step: int @param step: (Optional, def=1) Step count between min and max reps @type variable: Sulley Integer Primitive @param variable: (Optional, def=None) An integer primitive which will specify the number of repitions - @type fuzzable: Boolean + @type fuzzable: bool @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type name: String + @type name: str @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' + """ - repeat = blocks.repeat(block_name, blocks.CURRENT, min_reps, max_reps, step, variable, fuzzable, name) + repeat = blocks.Repeat(block_name, blocks.CURRENT, min_reps, max_reps, step, variable, fuzzable, name) blocks.CURRENT.push(repeat) -def s_size (block_name, length=4, endian="<", format="binary", inclusive=False, signed=False, math=None, fuzzable=False, name=None): - ''' +def s_size(block_name, length=4, endian=LITTLE_ENDIAN, output_format="binary", inclusive=False, signed=False, math=None, + fuzzable=False, name=None): + """ Create a sizer block bound to the block with the specified name. You *can not* create a sizer for any currently open blocks. @see: Aliases: s_sizer() - @type block_name: String - @param block_name: Name of block to apply sizer to - @type length: Integer - @param length: (Optional, def=4) Length of sizer - @type endian: Character - @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) - @type format: String - @param format: (Optional, def=binary) Output format, "binary" or "ascii" - @type inclusive: Boolean - @param inclusive: (Optional, def=False) Should the sizer count its own length? - @type signed: Boolean - @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") - @type math: Function - @param math: (Optional, def=None) Apply the mathematical operations defined in this function to the size - @type fuzzable: Boolean - @param fuzzable: (Optional, def=False) Enable/disable fuzzing of this sizer - @type name: String - @param name: Name of this sizer field - ''' + @type block_name: str + @param block_name: Name of block to apply sizer to + @type length: int + @param length: (Optional, def=4) Length of sizer + @type endian: Character + @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) + @type output_format: str + @param output_format: (Optional, def=binary) Output format, "binary" or "ascii" + @type inclusive: bool + @param inclusive: (Optional, def=False) Should the sizer count its own length? + @type signed: bool + @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") + @type math: Function + @param math: (Optional, def=None) Apply the mathematical operations defined in this function to the size + @type fuzzable: bool + @param fuzzable: (Optional, def=False) Enable/disable fuzzing of this sizer + @type name: str + @param name: Name of this sizer field + """ # you can't add a size for a block currently in the stack. if block_name in blocks.CURRENT.block_stack: raise sex.SullyRuntimeError("CAN NOT ADD A SIZE FOR A BLOCK CURRENTLY IN THE STACK") - size = blocks.size(block_name, blocks.CURRENT, length, endian, format, inclusive, signed, math, fuzzable, name) + size = blocks.Size( + block_name, blocks.CURRENT, length, endian, output_format, inclusive, signed, math, fuzzable, name + ) blocks.CURRENT.push(size) -def s_update (name, value): - ''' +def s_update(name, value): + """ Update the value of the named primitive in the currently open request. - @type name: String + @type name: str @param name: Name of object whose value we wish to update @type value: Mixed @param value: Updated value - ''' + """ - if not blocks.CURRENT.names.has_key(name): + if not name in blocks.CURRENT: raise sex.SullyRuntimeError("NO OBJECT WITH NAME '%s' FOUND IN CURRENT REQUEST" % name) blocks.CURRENT.names[name].value = value -######################################################################################################################## ### PRIMITIVES -######################################################################################################################## -def s_binary (value, name=None): - ''' + +def s_binary(value, name=None): + """ Parse a variable format binary string into a static value and push it onto the current block stack. - @type value: String + @type value: str @param value: Variable format binary string - @type name: String + @type name: str @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' + """ # parse the binary string into. parsed = value - parsed = parsed.replace(" ", "") - parsed = parsed.replace("\t", "") - parsed = parsed.replace("\r", "") - parsed = parsed.replace("\n", "") - parsed = parsed.replace(",", "") - parsed = parsed.replace("0x", "") + parsed = parsed.replace(" ", "") + parsed = parsed.replace("\t", "") + parsed = parsed.replace("\r", "") + parsed = parsed.replace("\n", "") + parsed = parsed.replace(",", "") + parsed = parsed.replace("0x", "") parsed = parsed.replace("\\x", "") value = "" while parsed: - pair = parsed[:2] + pair = parsed[:2] parsed = parsed[2:] value += chr(int(pair, 16)) - static = primitives.static(value, name) + static = primitives.Static(value, name) blocks.CURRENT.push(static) -def s_delim (value, fuzzable=True, name=None): - ''' +def s_delim(value, fuzzable=True, name=None): + """ Push a delimiter onto the current block stack. @type value: Character @param value: Original value - @type fuzzable: Boolean + @type fuzzable: bool @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type name: String + @type name: str @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' + """ - delim = primitives.delim(value, fuzzable, name) + delim = primitives.Delim(value, fuzzable, name) blocks.CURRENT.push(delim) -def s_group (name, values): - ''' +def s_group(name, values): + """ This primitive represents a list of static values, stepping through each one on mutation. You can tie a block to a group primitive to specify that the block should cycle through all possible mutations for *each* value within the group. The group primitive is useful for example for representing a list of valid opcodes. - @type name: String + @type name: str @param name: Name of group @type values: List or raw data @param values: List of possible raw values this group can take. - ''' + """ - group = primitives.group(name, values) + group = primitives.Group(name, values) blocks.CURRENT.push(group) -def s_lego (lego_type, value=None, options={}): - ''' +# noinspection PyCallingNonCallable +def s_lego(lego_type, value=None, options=()): + """ Legos are pre-built blocks... TODO: finish this doc - ''' + + @type lego_type: str + @param lego_type: Function that represents a lego + """ # as legos are blocks they must have a name. # generate a unique name for this lego. name = "LEGO_%08x" % len(blocks.CURRENT.names) - if not legos.BIN.has_key(lego_type): + if not lego_type in legos.BIN: raise sex.SullyRuntimeError("INVALID LEGO TYPE SPECIFIED: %s" % lego_type) - lego = legos.BIN[lego_type](name, blocks.CURRENT, value, options) # push the lego onto the stack and immediately pop to close the block. @@ -345,311 +343,320 @@ def s_lego (lego_type, value=None, options={}): blocks.CURRENT.pop() -def s_random (value, min_length, max_length, num_mutations=25, fuzzable=True, step=None, name=None): - ''' +def s_random(value, min_length, max_length, num_mutations=25, fuzzable=True, step=None, name=None): + """ Generate a random chunk of data while maintaining a copy of the original. A random length range can be specified. For a static length, set min/max length to be the same. @type value: Raw @param value: Original value - @type min_length: Integer + @type min_length: int @param min_length: Minimum length of random block - @type max_length: Integer + @type max_length: int @param max_length: Maximum length of random block - @type num_mutations: Integer + @type num_mutations: int @param num_mutations: (Optional, def=25) Number of mutations to make before reverting to default - @type fuzzable: Boolean + @type fuzzable: bool @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type step: Integer + @type step: int @param step: (Optional, def=None) If not null, step count between min and max reps, otherwise random - @type name: String + @type name: str @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' + """ - random = primitives.random_data(value, min_length, max_length, num_mutations, fuzzable, step, name) + random = primitives.RandomData(value, min_length, max_length, num_mutations, fuzzable, step, name) blocks.CURRENT.push(random) -def s_static (value, name=None): - ''' +def s_static(value, name=None): + """ Push a static value onto the current block stack. @see: Aliases: s_dunno(), s_raw(), s_unknown() @type value: Raw @param value: Raw static data - @type name: String + @type name: str @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' + """ - static = primitives.static(value, name) + static = primitives.Static(value, name) blocks.CURRENT.push(static) -def s_string (value, size=-1, padding="\x00", encoding="ascii", fuzzable=True, max_len=0, name=None): - ''' +def s_string(value, size=-1, padding="\x00", encoding="ascii", fuzzable=True, max_len=0, name=None): + """ Push a string onto the current block stack. - @type value: String + @type value: str @param value: Default string value - @type size: Integer + @type size: int @param size: (Optional, def=-1) Static size of this field, leave -1 for dynamic. @type padding: Character @param padding: (Optional, def="\\x00") Value to use as padding to fill static field size. - @type encoding: String + @type encoding: str @param encoding: (Optonal, def="ascii") String encoding, ex: utf_16_le for Microsoft Unicode. - @type fuzzable: Boolean + @type fuzzable: bool @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type max_len: Integer + @type max_len: int @param max_len: (Optional, def=0) Maximum string length - @type name: String + @type name: str @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' + """ - s = primitives.string(value, size, padding, encoding, fuzzable, max_len, name) + s = primitives.String(value, size, padding, encoding, fuzzable, max_len, name) blocks.CURRENT.push(s) -def s_bit_field (value, width, endian="<", format="binary", signed=False, full_range=False, fuzzable=True, name=None): - ''' +# noinspection PyTypeChecker +def s_bit_field(value, width, endian=LITTLE_ENDIAN, output_format="binary", signed=False, full_range=False, + fuzzable=True, name=None): + """ Push a variable length bit field onto the current block stack. @see: Aliases: s_bit(), s_bits() - @type value: Integer - @param value: Default integer value - @type width: Integer - @param width: Width of bit fields - @type endian: Character - @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) - @type format: String - @param format: (Optional, def=binary) Output format, "binary" or "ascii" - @type signed: Boolean - @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") - @type full_range: Boolean - @param full_range: (Optional, def=False) If enabled the field mutates through *all* possible values. - @type fuzzable: Boolean - @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type name: String - @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' - - bit_field = primitives.bit_field(value, width, None, endian, format, signed, full_range, fuzzable, name) + @type value: int + @param value: Default integer value + @type width: int + @param width: Width of bit fields + @type endian: Character + @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) + @type output_format: str + output_format format: (Optional, def=binary) Output format, "binary" or "ascii" + @type signed: bool + @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") + @type full_range: bool + @param full_range: (Optional, def=False) If enabled the field mutates through *all* possible values. + @type fuzzable: bool + @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive + @type name: str + @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive + """ + + bit_field = primitives.BitField(value, width, None, endian, output_format, signed, full_range, fuzzable, name) blocks.CURRENT.push(bit_field) -def s_byte (value, endian="<", format="binary", signed=False, full_range=False, fuzzable=True, name=None): - ''' +def s_byte(value, endian=LITTLE_ENDIAN, output_format="binary", signed=False, full_range=False, fuzzable=True, + name=None): + """ Push a byte onto the current block stack. @see: Aliases: s_char() - @type value: Integer - @param value: Default integer value - @type endian: Character - @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) - @type format: String - @param format: (Optional, def=binary) Output format, "binary" or "ascii" - @type signed: Boolean - @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") - @type full_range: Boolean - @param full_range: (Optional, def=False) If enabled the field mutates through *all* possible values. - @type fuzzable: Boolean - @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type name: String - @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' + @type value: int|str + @param value: Default integer value + @type endian: Character + @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) + @type output_format: str + @param output_format: (Optional, def=binary) Output format, "binary" or "ascii" + @type signed: bool + @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") + @type full_range: bool + @param full_range: (Optional, def=False) If enabled the field mutates through *all* possible values. + @type fuzzable: bool + @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive + @type name: str + @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive + """ - byte = primitives.byte(value, endian, format, signed, full_range, fuzzable, name) + byte = primitives.Byte(value, endian, output_format, signed, full_range, fuzzable, name) blocks.CURRENT.push(byte) -def s_word (value, endian="<", format="binary", signed=False, full_range=False, fuzzable=True, name=None): - ''' +def s_word(value, endian=LITTLE_ENDIAN, output_format="binary", signed=False, full_range=False, fuzzable=True, + name=None): + """ Push a word onto the current block stack. @see: Aliases: s_short() - @type value: Integer - @param value: Default integer value - @type endian: Character - @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) - @type format: String - @param format: (Optional, def=binary) Output format, "binary" or "ascii" - @type signed: Boolean - @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") - @type full_range: Boolean - @param full_range: (Optional, def=False) If enabled the field mutates through *all* possible values. - @type fuzzable: Boolean - @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type name: String - @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' + @type value: int + @param value: Default integer value + @type endian: chr + @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) + @type output_format: str + @param output_format: (Optional, def=binary) Output format, "binary" or "ascii" + @type signed: bool + @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") + @type full_range: bool + @param full_range: (Optional, def=False) If enabled the field mutates through *all* possible values. + @type fuzzable: bool + @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive + @type name: str + @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive + """ - word = primitives.word(value, endian, format, signed, full_range, fuzzable, name) + word = primitives.Word(value, endian, output_format, signed, full_range, fuzzable, name) blocks.CURRENT.push(word) -def s_dword (value, endian="<", format="binary", signed=False, full_range=False, fuzzable=True, name=None): - ''' +def s_dword(value, endian=LITTLE_ENDIAN, output_format="binary", signed=False, full_range=False, fuzzable=True, + name=None): + """ Push a double word onto the current block stack. @see: Aliases: s_long(), s_int() - @type value: Integer - @param value: Default integer value - @type endian: Character - @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) - @type format: String - @param format: (Optional, def=binary) Output format, "binary" or "ascii" - @type signed: Boolean - @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") - @type full_range: Boolean - @param full_range: (Optional, def=False) If enabled the field mutates through *all* possible values. - @type fuzzable: Boolean - @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type name: String - @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' + @type value: int + @param value: Default integer value + @type endian: Character + @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) + @type output_format: str + @param output_format: (Optional, def=binary) Output format, "binary" or "ascii" + @type signed: bool + @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") + @type full_range: bool + @param full_range: (Optional, def=False) If enabled the field mutates through *all* possible values. + @type fuzzable: bool + @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive + @type name: str + @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive + """ - dword = primitives.dword(value, endian, format, signed, full_range, fuzzable, name) + dword = primitives.DWord(value, endian, output_format, signed, full_range, fuzzable, name) blocks.CURRENT.push(dword) -def s_qword (value, endian="<", format="binary", signed=False, full_range=False, fuzzable=True, name=None): - ''' +def s_qword(value, endian=LITTLE_ENDIAN, output_format="binary", signed=False, full_range=False, fuzzable=True, + name=None): + """ Push a quad word onto the current block stack. @see: Aliases: s_double() - @type value: Integer - @param value: Default integer value - @type endian: Character - @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) - @type format: String - @param format: (Optional, def=binary) Output format, "binary" or "ascii" - @type signed: Boolean - @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") - @type full_range: Boolean - @param full_range: (Optional, def=False) If enabled the field mutates through *all* possible values. - @type fuzzable: Boolean - @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type name: String - @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' + @type value: int + @param value: Default integer value + @type endian: Character + @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) + @type output_format: str + @param output_format: (Optional, def=binary) Output format, "binary" or "ascii" + @type signed: bool + @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") + @type full_range: bool + @param full_range: (Optional, def=False) If enabled the field mutates through *all* possible values. + @type fuzzable: bool + @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive + @type name: str + @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive + """ - qword = primitives.qword(value, endian, format, signed, full_range, fuzzable, name) + qword = primitives.QWord(value, endian, output_format, signed, full_range, fuzzable, name) blocks.CURRENT.push(qword) -######################################################################################################################## ### ALIASES -######################################################################################################################## - -s_dunno = s_raw = s_unknown = s_static -s_sizer = s_size -s_bit = s_bits = s_bit_field -s_char = s_byte -s_short = s_word -s_long = s_int = s_dword -s_double = s_qword + + +s_dunno = s_raw = s_unknown = s_static +s_sizer = s_size +s_bit = s_bits = s_bit_field +s_char = s_byte +s_short = s_word +s_long = s_int = s_dword +s_double = s_qword s_repeater = s_repeat +s_intelword = lambda x: s_long(x, endian=LITTLE_ENDIAN) +s_intelhalfword = lambda x: s_short(x, endian=LITTLE_ENDIAN) +s_bigword = lambda x: s_long(x, endian=BIG_ENDIAN) +s_unistring = lambda x: s_string(x, encoding="utf_16_le") + -### SPIKE Aliases - -def custom_raise (argument, msg): - def _(x): - raise msg, argument(x) - return _ - -s_intelword = lambda x: s_long(x, endian=LITTLE_ENDIAN) -s_intelhalfword = lambda x: s_short(x, endian=LITTLE_ENDIAN) -s_bigword = lambda x: s_long(x, endian=BIG_ENDIAN) -s_string_lf = custom_raise(ValueError, "NotImplementedError: s_string_lf is not currently implemented, arguments were") -s_string_or_env = custom_raise(ValueError, "NotImplementedError: s_string_or_env is not currently implemented, arguments were") -s_string_repeat = custom_raise(ValueError, "NotImplementedError: s_string_repeat is not currently implemented, arguments were") -s_string_variable = custom_raise(ValueError, "NotImplementedError: s_string_variable is not currently implemented, arguments were") -s_string_variables = custom_raise(ValueError, "NotImplementedError: s_string_variables is not currently implemented, arguments were") -s_binary_repeat = custom_raise(ValueError, "NotImplementedError: s_string_variables is not currently implemented, arguments were") -s_unistring = lambda x: s_string(x, encoding="utf_16_le") -s_unistring_variable = custom_raise(ValueError, "NotImplementedError: s_unistring_variable is not currently implemented, arguments were") -s_xdr_string = custom_raise(ValueError, "LegoNotUtilizedError: XDR strings are available in the XDR lego, arguments were") - -def s_cstring (x): +def s_cstring(x): s_string(x) s_static("\x00") -s_binary_block_size_intel_halfword_plus_variable = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_halfword_bigendian_variable = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_word_bigendian_plussome = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_word_bigendian_variable = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_halfword_bigendian_mult = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_intel_halfword_variable = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_intel_halfword_mult = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_intel_halfword_plus = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_halfword_bigendian = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_word_intel_mult_plus = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_intel_word_variable = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_word_bigendian_mult = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_blocksize_unsigned_string_variable = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_intel_word_plus = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_intel_halfword = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_word_bigendian = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_blocksize_signed_string_variable = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_byte_variable = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_intel_word = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_byte_plus = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_byte_mult = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_blocksize_asciihex_variable = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_binary_block_size_byte = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_blocksize_asciihex = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") -s_blocksize_string = custom_raise(ValueError, "SizerNotUtilizedError: Use the s_size primitive for including sizes, arguments were") - - - -######################################################################################################################## + +# Not implemented aliases yet +def not_impl(alias, args): + raise NotImplementedError("%s isn't implemented yet. Args -> %s" % (alias, args)) + + +s_string_lf = lambda args: not_impl("s_string_lf", args) +s_string_or_env = lambda args: not_impl("s_string_or_env", args) +s_string_repeat = lambda args: not_impl("s_string_repeat", args) +s_string_variable = lambda args: not_impl("s_string_variable", args) +s_string_variables = lambda args: not_impl("s_string_variables", args) +s_binary_repeat = lambda args: not_impl("s_binary_repeat", args) +s_unistring_variable = lambda args: not_impl("s_unistring_variable", args) +s_xdr_string = lambda args: not_impl("s_xdr_string", args) + + +def no_sizer(args): + raise sex.SizerNotUtilizedError("Use the s_size primitive for including sizes. Args -> %s" % args) + + +# A bunch of un-defined primitives from SPIKE +s_binary_block_size_intel_halfword_plus_variable = lambda args: no_sizer(args) +s_binary_block_size_halfword_bigendian_variable = lambda args: no_sizer(args) +s_binary_block_size_word_bigendian_plussome = lambda args: no_sizer(args) +s_binary_block_size_word_bigendian_variable = lambda args: no_sizer(args) +s_binary_block_size_halfword_bigendian_mult = lambda args: no_sizer(args) +s_binary_block_size_intel_halfword_variable = lambda args: no_sizer(args) +s_binary_block_size_intel_halfword_mult = lambda args: no_sizer(args) +s_binary_block_size_intel_halfword_plus = lambda args: no_sizer(args) +s_binary_block_size_halfword_bigendian = lambda args: no_sizer(args) +s_binary_block_size_word_intel_mult_plus = lambda args: no_sizer(args) +s_binary_block_size_intel_word_variable = lambda args: no_sizer(args) +s_binary_block_size_word_bigendian_mult = lambda args: no_sizer(args) +s_blocksize_unsigned_string_variable = lambda args: no_sizer(args) +s_binary_block_size_intel_word_plus = lambda args: no_sizer(args) +s_binary_block_size_intel_halfword = lambda args: no_sizer(args) +s_binary_block_size_word_bigendian = lambda args: no_sizer(args) +s_blocksize_signed_string_variable = lambda args: no_sizer(args) +s_binary_block_size_byte_variable = lambda args: no_sizer(args) +s_binary_block_size_intel_word = lambda args: no_sizer(args) +s_binary_block_size_byte_plus = lambda args: no_sizer(args) +s_binary_block_size_byte_mult = lambda args: no_sizer(args) +s_blocksize_asciihex_variable = lambda args: no_sizer(args) +s_binary_block_size_byte = lambda args: no_sizer(args) +s_blocksize_asciihex = lambda args: no_sizer(args) +s_blocksize_string = lambda args: no_sizer(args) + + ### MISC -######################################################################################################################## -def s_hex_dump (data, addr=0): - ''' + +def s_hex_dump(data, addr=0): + """ Return the hex dump of the supplied data starting at the offset address specified. @type data: Raw @param data: Data to show hex dump of - @type addr: Integer + @type addr: int @param addr: (Optional, def=0) Offset to start displaying hex dump addresses from - @rtype: String + @rtype: str @return: Hex dump of raw data - ''' + """ - dump = slice = "" + dump = byte_slice = "" for byte in data: if addr % 16 == 0: dump += " " - for char in slice: - if ord(char) >= 32 and ord(char) <= 126: + for char in byte_slice: + if 32 <= ord(char) <= 126: dump += char else: dump += "." dump += "\n%04x: " % addr - slice = "" + byte_slice = "" - dump += "%02x " % ord(byte) - slice += byte - addr += 1 + dump += "%02x " % ord(byte) + byte_slice += byte + addr += 1 remainder = addr % 16 if remainder != 0: dump += " " * (16 - remainder) + " " - for char in slice: - if ord(char) >= 32 and ord(char) <= 126: + for char in byte_slice: + if 32 <= ord(char) <= 126: dump += char else: dump += "." diff --git a/sulley/blocks.py b/sulley/blocks.py index 7ad88cb..83dfc75 100644 --- a/sulley/blocks.py +++ b/sulley/blocks.py @@ -1,48 +1,43 @@ -import pgraph import primitives import sex - import zlib import hashlib import struct +from constants import LITTLE_ENDIAN REQUESTS = {} -CURRENT = None +CURRENT = None + -######################################################################################################################## -class request (pgraph.node): - def __init__ (self, name): - ''' +class Request(object): + def __init__(self, name): + """ Top level container instantiated by s_initialize(). Can hold any block structure or primitive. This can essentially be thought of as a super-block, root-block, daddy-block or whatever other alias you prefer. - @type name: String + @type name: str @param name: Name of this request - ''' - - self.name = name - - self.label = name # node label for graph rendering. - self.stack = [] # the request stack. - self.block_stack = [] # list of open blocks, -1 is last open block. - self.closed_blocks = {} # dictionary of closed blocks. - self.callbacks = {} # dictionary of list of sizers / checksums that were unable to complete rendering. - self.names = {} # dictionary of directly accessible primitives. - self.rendered = "" # rendered block structure. - self.mutant_index = 0 # current mutation index. - self.mutant = None # current primitive being mutated. - - - def mutate (self): + """ + + self.name = name + self.label = name # node label for graph rendering. + self.stack = [] # the request stack. + self.block_stack = [] # list of open blocks, -1 is last open block. + self.closed_blocks = {} # dictionary of closed blocks. + self.callbacks = {} # dictionary of list of sizers / checksums that were unable to complete rendering. + self.names = {} # dictionary of directly accessible primitives. + self.rendered = "" # rendered block structure. + self.mutant_index = 0 # current mutation index. + self.mutant = None # current primitive being mutated. + + def mutate(self): mutated = False for item in self.stack: if item.fuzzable and item.mutate(): mutated = True - - if not isinstance(item, block): + if not isinstance(item, Block): self.mutant = item - break if mutated: @@ -50,14 +45,13 @@ def mutate (self): return mutated - - def num_mutations (self): - ''' + def num_mutations(self): + """ Determine the number of repetitions we will be making. - @rtype: Integer + @rtype: int @return: Number of mutated forms this primitive can take. - ''' + """ num_mutations = 0 @@ -67,23 +61,21 @@ def num_mutations (self): return num_mutations - - def pop (self): - ''' + def pop(self): + """ The last open block was closed, so pop it off of the block stack. - ''' + """ if not self.block_stack: raise sex.SullyRuntimeError("BLOCK STACK OUT OF SYNC") self.block_stack.pop() - - def push (self, item): - ''' + def push(self, item): + """ Push an item into the block structure. If no block is open, the item goes onto the request stack. otherwise, the item goes onto the last open blocks stack. - ''' + """ # if the item has a name, add it to the internal dictionary of names. if hasattr(item, "name") and item.name: @@ -101,11 +93,10 @@ def push (self, item): self.block_stack[-1].push(item) # add the opened block to the block stack. - if isinstance(item, block): + if isinstance(item, Block): self.block_stack.append(item) - - def render (self): + def render(self): # ensure there are no open blocks lingering. if self.block_stack: raise sex.SullyRuntimeError("UNCLOSED BLOCK: %s" % self.block_stack[-1].name) @@ -119,15 +110,16 @@ def render (self): for item in self.callbacks[key]: item.render() + # noinspection PyUnusedLocal def update_size(stack, name): # walk recursively through each block to update its size blocks = [] - for item in stack: - if isinstance(item, size): - item.render() - elif isinstance(item, block): - blocks += [item] + for stack_item in stack: + if isinstance(stack_item, Size): + stack_item.render() + elif isinstance(stack_item, Block): + blocks += [stack_item] for b in blocks: update_size(b.stack, b.name) @@ -135,7 +127,7 @@ def update_size(stack, name): # call update_size on each block of the request for item in self.stack: - if isinstance(item, block): + if isinstance(item, Block): update_size(item.stack, item.name) item.render() @@ -147,82 +139,82 @@ def update_size(stack, name): return self.rendered - - def reset (self): - ''' + def reset(self): + """ Reset every block and primitives mutant state under this request. - ''' + """ - self.mutant_index = 1 + self.mutant_index = 1 self.closed_blocks = {} for item in self.stack: if item.fuzzable: item.reset() - - def walk (self, stack=None): - ''' + def walk(self, stack=None): + """ Recursively walk through and yield every primitive and block on the request stack. @rtype: Sulley Primitives @return: Sulley Primitives - ''' + """ if not stack: stack = self.stack for item in stack: # if the item is a block, step into it and continue looping. - if isinstance(item, block): - for item in self.walk(item.stack): - yield item + if isinstance(item, Block): + for stack_item in self.walk(item.stack): + yield stack_item else: yield item + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.name) + -######################################################################################################################## -class block: - def __init__ (self, name, request, group=None, encoder=None, dep=None, dep_value=None, dep_values=[], dep_compare="=="): - ''' +class Block(object): + def __init__(self, name, request, group=None, encoder=None, dep=None, dep_value=None, dep_values=None, + dep_compare="=="): + """ The basic building block. Can contain primitives, sizers, checksums or other blocks. - @type name: String + @type name: str @param name: Name of the new block @type request: s_request @param request: Request this block belongs to - @type group: String + @type group: str @param group: (Optional, def=None) Name of group to associate this block with @type encoder: Function Pointer @param encoder: (Optional, def=None) Optional pointer to a function to pass rendered data to prior to return - @type dep: String + @type dep: str @param dep: (Optional, def=None) Optional primitive whose specific value this block is dependant on @type dep_value: Mixed @param dep_value: (Optional, def=None) Value that field "dep" must contain for block to be rendered @type dep_values: List of Mixed Types @param dep_values: (Optional, def=[]) Values that field "dep" may contain for block to be rendered - @type dep_compare: String + @type dep_compare: str @param dep_compare: (Optional, def="==") Comparison method to apply to dependency (==, !=, >, >=, <, <=) - ''' - - self.name = name - self.request = request - self.group = group - self.encoder = encoder - self.dep = dep - self.dep_value = dep_value - self.dep_values = dep_values - self.dep_compare = dep_compare - - self.stack = [] # block item stack. - self.rendered = "" # rendered block contents. - self.fuzzable = True # blocks are always fuzzable because they may contain fuzzable items. - self.group_idx = 0 # if this block is tied to a group, the index within that group. + """ + + self.name = name + self.request = request + self.group = group + self.encoder = encoder + self.dep = dep + self.dep_value = dep_value + self.dep_values = dep_values + self.dep_compare = dep_compare + + self.stack = [] # block item stack. + self.rendered = "" # rendered block contents. + self.fuzzable = True # blocks are always fuzzable because they may contain fuzzable items. + self.group_idx = 0 # if this block is tied to a group, the index within that group. self.fuzz_complete = False # whether or not we are done fuzzing this block. - self.mutant_index = 0 # current mutation index. + self.mutant_index = 0 # current mutation index. - - def mutate (self): + def mutate(self): mutated = False # are we done with this block? @@ -232,7 +224,6 @@ def mutate (self): # # mutate every item on the stack for every possible group value. # - if self.group: group_count = self.request.names[self.group].num_mutations() @@ -244,9 +235,8 @@ def mutate (self): if item.fuzzable and item.mutate(): mutated = True - if not isinstance(item, block): + if not isinstance(item, Block): self.request.mutant = item - break # if the possible mutations for the stack are exhausted. @@ -276,21 +266,19 @@ def mutate (self): if item.fuzzable and item.mutate(): mutated = True - if not isinstance(item, block): + if not isinstance(item, Block): self.request.mutant = item break - # # no grouping, mutate every item on the stack once. # - else: for item in self.stack: if item.fuzzable and item.mutate(): mutated = True - if not isinstance(item, block): + if not isinstance(item, Block): self.request.mutant = item break @@ -306,29 +294,23 @@ def mutate (self): else: self.request.names[self.dep].value = self.dep_value - # we are done mutating this block. if not mutated: self.fuzz_complete = True - # if we had a dependancy, make sure we restore the original value. + # if we had a dependency, make sure we restore the original value. if self.dep: self.request.names[self.dep].value = self.request.names[self.dep].original_value - if mutated: - if not isinstance(item, block): - self.request.mutant = item - return mutated - - def num_mutations (self): - ''' + def num_mutations(self): + """ Determine the number of repetitions we will be making. - @rtype: Integer + @rtype: int @return: Number of mutated forms this primitive can take. - ''' + """ num_mutations = 0 @@ -342,19 +324,17 @@ def num_mutations (self): return num_mutations - - def push (self, item): - ''' + def push(self, item): + """ Push an arbitrary item onto this blocks stack. - ''' + """ self.stack.append(item) - - def render (self): - ''' + def render(self): + """ Step through every item on this blocks stack and render it. Subsequent blocks recursively render their stacks. - ''' + """ # add the completed block to the request dictionary. self.request.closed_blocks[self.name] = self @@ -417,63 +397,68 @@ def render (self): self.rendered = self.encoder(self.rendered) # the block is now closed, clear out all the entries from the request back splice dictionary. - if self.request.callbacks.has_key(self.name): + if self.name in self.request.callbacks: for item in self.request.callbacks[self.name]: item.render() - - def reset (self): - ''' + def reset(self): + """ Reset the primitives on this blocks stack to the starting mutation state. - ''' + """ self.fuzz_complete = False - self.group_idx = 0 + self.group_idx = 0 for item in self.stack: if item.fuzzable: item.reset() + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.name) + -######################################################################################################################## -class checksum: - checksum_lengths = {"crc32":4, "adler32":4, "md5":16, "sha1":20} +class Checksum: + checksum_lengths = { + "crc32": 4, + "adler32": 4, + "md5": 16, + "sha1": 20 + } - def __init__(self, block_name, request, algorithm="crc32", length=0, endian="<", name=None): - ''' - Create a checksum block bound to the block with the specified name. You *can not* create a checksm for any + def __init__(self, block_name, request, algorithm="crc32", length=0, endian=LITTLE_ENDIAN, name=None): + """ + Create a checksum block bound to the block with the specified name. You *can not* create a checksum for any currently open blocks. - @type block_name: String + @type block_name: str @param block_name: Name of block to apply sizer to @type request: s_request @param request: Request this block belongs to - @type algorithm: String + @type algorithm: str or def @param algorithm: (Optional, def=crc32) Checksum algorithm to use. (crc32, adler32, md5, sha1) - @type length: Integer + @type length: int @param length: (Optional, def=0) Length of checksum, specify 0 to auto-calculate @type endian: Character @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) - @type name: String + @type name: str @param name: Name of this checksum field - ''' + """ self.block_name = block_name - self.request = request - self.algorithm = algorithm - self.length = length - self.endian = endian - self.name = name + self.request = request + self.algorithm = algorithm + self.length = length + self.endian = endian + self.name = name - self.rendered = "" - self.fuzzable = False + self.rendered = "" + self.fuzzable = False - if not self.length and self.checksum_lengths.has_key(self.algorithm): + if not self.length and self.algorithm in self.checksum_lengths: self.length = self.checksum_lengths[self.algorithm] - - def checksum (self, data): - ''' + def checksum(self, data): + """ Calculate and return the checksum (in raw bytes) over the supplied data. @type data: Raw @@ -481,14 +466,14 @@ def checksum (self, data): @rtype: Raw @return: Checksum. - ''' + """ if type(self.algorithm) is str: if self.algorithm == "crc32": - return struct.pack(self.endian+"L", zlib.crc32(data)) + return struct.pack(self.endian + "L", zlib.crc32(data)) elif self.algorithm == "adler32": - return struct.pack(self.endian+"L", zlib.adler32(data)) + return struct.pack(self.endian + "L", zlib.adler32(data)) elif self.algorithm == "md5": digest = hashlib.md5(data).digest() @@ -496,7 +481,7 @@ def checksum (self, data): # TODO: is this right? if self.endian == ">": (a, b, c, d) = struct.unpack("LLLL", a, b, c, d) + digest = struct.pack(">LLLL", a, b, c, d) return digest @@ -506,7 +491,7 @@ def checksum (self, data): # TODO: is this right? if self.endian == ">": (a, b, c, d, e) = struct.unpack("LLLLL", a, b, c, d, e) + digest = struct.pack(">LLLLL", a, b, c, d, e) return digest @@ -515,111 +500,120 @@ def checksum (self, data): else: return self.algorithm(data) - - def render (self): - ''' + def render(self): + """ Calculate the checksum of the specified block using the specified algorithm. - ''' + """ self.rendered = "" # if the target block for this sizer is already closed, render the checksum. if self.block_name in self.request.closed_blocks: - block_data = self.request.closed_blocks[self.block_name].rendered + block_data = self.request.closed_blocks[self.block_name].rendered self.rendered = self.checksum(block_data) # otherwise, add this checksum block to the requests callback list. else: - if not self.request.callbacks.has_key(self.block_name): + if not self.block_name in self.request.callbacks: self.request.callbacks[self.block_name] = [] self.request.callbacks[self.block_name].append(self) + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.name) -######################################################################################################################## -class repeat: - ''' + +class Repeat: + """ This block type is kind of special in that it is a hybrid between a block and a primitive (it can be fuzzed). The user does not need to be wary of this fact. - ''' + """ - def __init__ (self, block_name, request, min_reps=0, max_reps=None, step=1, variable=None, fuzzable=True, name=None): - ''' + def __init__(self, block_name, request, min_reps=0, max_reps=None, step=1, variable=None, fuzzable=True, name=None): + """ Repeat the rendered contents of the specified block cycling from min_reps to max_reps counting by step. By default renders to nothing. This block modifier is useful for fuzzing overflows in table entries. This block modifier MUST come after the block it is being applied to. - @type block_name: String + @type block_name: str @param block_name: Name of block to apply sizer to @type request: s_request @param request: Request this block belongs to - @type min_reps: Integer + @type min_reps: int @param min_reps: (Optional, def=0) Minimum number of block repetitions - @type max_reps: Integer + @type max_reps: int @param max_reps: (Optional, def=None) Maximum number of block repetitions - @type step: Integer + @type step: int @param step: (Optional, def=1) Step count between min and max reps @type variable: Sulley Integer Primitive - @param variable: (Optional, def=None) Repititions will be derived from this variable, disables fuzzing - @type fuzzable: Boolean + @param variable: (Optional, def=None) Repetitions will be derived from this variable, disables fuzzing + @type fuzzable: bool @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type name: String + @type name: str @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' - - self.block_name = block_name - self.request = request - self.variable = variable - self.min_reps = min_reps - self.max_reps = max_reps - self.step = step - self.fuzzable = fuzzable - self.name = name - - self.value = self.original_value = "" # default to nothing! - self.rendered = "" # rendered value - self.fuzz_complete = False # flag if this primitive has been completely fuzzed - self.fuzz_library = [] # library of static fuzz heuristics to cycle through. - self.mutant_index = 0 # current mutation number - self.current_reps = min_reps # current number of repetitions + """ + + self.block_name = block_name + self.request = request + self.variable = variable + self.min_reps = min_reps + self.max_reps = max_reps + self.step = step + self.fuzzable = fuzzable + self.name = name + + self.value = "" + self.original_value = "" # default to nothing! + self.rendered = "" # rendered value + self.fuzz_complete = False # flag if this primitive has been completely fuzzed + self.fuzz_library = [] # library of static fuzz heuristics to cycle through. + self.mutant_index = 0 # current mutation number + self.current_reps = min_reps # current number of repetitions # ensure the target block exists. if self.block_name not in self.request.names: - raise sex.SullyRuntimeError("CAN NOT ADD REPEATER FOR NON-EXISTANT BLOCK: %s" % self.block_name) + raise sex.SullyRuntimeError( + "Can't add repeater for non-existent block: %s!" % self.block_name + ) # ensure the user specified either a variable to tie this repeater to or a min/max val. - if self.variable == None and self.max_reps == None: - raise sex.SullyRuntimeError("REPEATER FOR BLOCK %s DOES NOT HAVE A MIN/MAX OR VARIABLE BINDING" % self.block_name) + if self.variable is None and self.max_reps is None: + raise sex.SullyRuntimeError( + "Repeater for block %s doesn't have a min/max or variable binding!" % self.block_name + ) # if a variable is specified, ensure it is an integer type. - if self.variable and not isinstance(self.variable, primitives.bit_field): + if self.variable and not isinstance(self.variable, primitives.BitField): print self.variable - raise sex.SullyRuntimeError("ATTEMPT TO BIND THE REPEATER FOR BLOCK %s TO A NON INTEGER PRIMITIVE" % self.block_name) + raise sex.SullyRuntimeError( + "Attempt to bind the repeater for block %s to a non-integer primitive!" % self.block_name + ) - # if not binding variable was specified, propogate the fuzz library with the repetition counts. + # if not binding variable was specified, propagate the fuzz library with the repetition counts. if not self.variable: self.fuzz_library = range(self.min_reps, self.max_reps + 1, self.step) - # otherwise, disable fuzzing as the repitition count is determined by the variable. + # otherwise, disable fuzzing as the repetition count is determined by the variable. else: self.fuzzable = False - - def mutate (self): - ''' + def mutate(self): + """ Mutate the primitive by stepping through the fuzz library, return False on completion. If variable-bounding is specified then fuzzing is implicitly disabled. Instead, the render() routine will properly calculate the - correct repitition and return the appropriate data. + correct repetition and return the appropriate data. - @rtype: Boolean + @rtype: bool @return: True on success, False otherwise. - ''' + """ # render the contents of the block we are repeating. self.request.names[self.block_name].render() # if the target block for this sizer is not closed, raise an exception. if self.block_name not in self.request.closed_blocks: - raise sex.SullyRuntimeError("CAN NOT APPLY REPEATER TO UNCLOSED BLOCK: %s" % self.block_name) + raise sex.SullyRuntimeError( + "Can't apply repeater to unclosed block: %s" % self.block_name + ) # if we've run out of mutations, raise the completion flag. if self.mutant_index == self.num_mutations(): @@ -627,7 +621,7 @@ def mutate (self): # if fuzzing was disabled or complete, and mutate() is called, ensure the original value is restored. if not self.fuzzable or self.fuzz_complete: - self.value = self.original_value + self.value = self.original_value self.current_reps = self.min_reps return False @@ -637,7 +631,7 @@ def mutate (self): self.current_reps = self.fuzz_library[self.mutant_index] # set the current value as a multiple of the rendered block based on the current fuzz library count. - block = self.request.closed_blocks[self.block_name] + block = self.request.closed_blocks[self.block_name] self.value = block.rendered * self.fuzz_library[self.mutant_index] # increment the mutation count. @@ -645,22 +639,20 @@ def mutate (self): return True - - def num_mutations (self): - ''' + def num_mutations(self): + """ Determine the number of repetitions we will be making. - @rtype: Integer + @rtype: int @return: Number of mutated forms this primitive can take. - ''' + """ return len(self.fuzz_library) - - def render (self): - ''' + def render(self): + """ Nothing fancy on render, simply return the value. - ''' + """ # if the target block for this sizer is not closed, raise an exception. if self.block_name not in self.request.closed_blocks: @@ -668,106 +660,111 @@ def render (self): # if a variable-bounding was specified then set the value appropriately. if self.variable: - block = self.request.closed_blocks[self.block_name] + block = self.request.closed_blocks[self.block_name] self.value = block.rendered * self.variable.value self.rendered = self.value return self.rendered - - def reset (self): - ''' + def reset(self): + """ Reset the fuzz state of this primitive. - ''' + """ + self.fuzz_complete = False + self.mutant_index = 0 + self.value = self.original_value - self.fuzz_complete = False - self.mutant_index = 0 - self.value = self.original_value + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.name) -######################################################################################################################## -class size: - ''' +class Size: + """ This block type is kind of special in that it is a hybrid between a block and a primitive (it can be fuzzed). The user does not need to be wary of this fact. - ''' + """ - def __init__ (self, block_name, request, length=4, endian="<", format="binary", inclusive=False, signed=False, math=None, fuzzable=False, name=None): - ''' + def __init__(self, block_name, request, length=4, endian="<", output_format="binary", inclusive=False, signed=False, + math=None, fuzzable=False, name=None): + """ Create a sizer block bound to the block with the specified name. You *can not* create a sizer for any currently open blocks. - @type block_name: String - @param block_name: Name of block to apply sizer to - @type request: s_request - @param request: Request this block belongs to - @type length: Integer - @param length: (Optional, def=4) Length of sizer - @type endian: Character - @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) - @type format: String - @param format: (Optional, def=binary) Output format, "binary" or "ascii" - @type inclusive: Boolean - @param inclusive: (Optional, def=False) Should the sizer count its own length? - @type signed: Boolean - @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") - @type math: Function - @param math: (Optional, def=None) Apply the mathematical operations defined in this function to the size - @type fuzzable: Boolean - @param fuzzable: (Optional, def=False) Enable/disable fuzzing of this sizer - @type name: String - @param name: Name of this sizer field - ''' - - self.block_name = block_name - self.request = request - self.length = length - self.endian = endian - self.format = format - self.inclusive = inclusive - self.signed = signed - self.math = math - self.fuzzable = fuzzable - self.name = name - - self.original_value = "N/A" # for get_primitive - self.s_type = "size" # for ease of object identification - self.bit_field = primitives.bit_field(0, self.length*8, endian=self.endian, format=self.format, signed=self.signed) - self.rendered = "" - self.fuzz_complete = self.bit_field.fuzz_complete - self.fuzz_library = self.bit_field.fuzz_library - self.mutant_index = self.bit_field.mutant_index - self.value = self.bit_field.value - - if self.math == None: - self.math = lambda (x): x + @type block_name: str + @param block_name: Name of block to apply sizer to + @type request: s_request + @param request: Request this block belongs to + @type length: int + @param length: (Optional, def=4) Length of sizer + @type endian: chr + @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) + @type output_format: str + @param output_format: (Optional, def=binary) Output format, "binary" or "ascii" + @type inclusive: bool + @param inclusive: (Optional, def=False) Should the sizer count its own length? + @type signed: bool + @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") + @type math: def + @param math: (Optional, def=None) Apply the mathematical op defined in this function to the size + @type fuzzable: bool + @param fuzzable: (Optional, def=False) Enable/disable fuzzing of this sizer + @type name: str + @param name: Name of this sizer field + """ + self.block_name = block_name + self.request = request + self.length = length + self.endian = endian + self.format = output_format + self.inclusive = inclusive + self.signed = signed + self.math = math + self.fuzzable = fuzzable + self.name = name + + self.original_value = "N/A" # for get_primitive + self.s_type = "size" # for ease of object identification + self.bit_field = primitives.BitField( + 0, + self.length * 8, + endian=self.endian, + output_format=self.format, + signed=self.signed + ) + self.rendered = "" + self.fuzz_complete = self.bit_field.fuzz_complete + self.fuzz_library = self.bit_field.fuzz_library + self.mutant_index = self.bit_field.mutant_index + self.value = self.bit_field.value + + if not self.math: + self.math = lambda (x): x - def exhaust (self): - ''' + def exhaust(self): + """ Exhaust the possible mutations for this primitive. - @rtype: Integer + @rtype: int @return: The number of mutations to reach exhaustion - ''' + """ num = self.num_mutations() - self.mutant_index - self.fuzz_complete = True - self.mutant_index = self.num_mutations() + self.fuzz_complete = True + self.mutant_index = self.num_mutations() self.bit_field.mutant_index = self.num_mutations() - self.value = self.original_value + self.value = self.original_value return num - - def mutate (self): - ''' + def mutate(self): + """ Wrap the mutation routine of the internal bit_field primitive. @rtype: Boolean @return: True on success, False otherwise. - ''' + """ if self.mutant_index == self.num_mutations(): self.fuzz_complete = True @@ -776,22 +773,20 @@ def mutate (self): return self.bit_field.mutate() - - def num_mutations (self): - ''' + def num_mutations(self): + """ Wrap the num_mutations routine of the internal bit_field primitive. - @rtype: Integer + @rtype: int @return: Number of mutated forms this primitive can take. - ''' + """ return self.bit_field.num_mutations() - - def render (self): - ''' + def render(self): + """ Render the sizer. - ''' + """ self.rendered = "" @@ -801,24 +796,28 @@ def render (self): # if the target block for this sizer is already closed, render the size. elif self.block_name in self.request.closed_blocks: - if self.inclusive: self_size = self.length - else: self_size = 0 + if self.inclusive: + self_size = self.length + else: + self_size = 0 - block = self.request.closed_blocks[self.block_name] + block = self.request.closed_blocks[self.block_name] self.bit_field.value = self.math(len(block.rendered) + self_size) - self.rendered = self.bit_field.render() + self.rendered = self.bit_field.render() # otherwise, add this sizer block to the requests callback list. else: - if not self.request.callbacks.has_key(self.block_name): + if not self.block_name in self.request.callbacks: self.request.callbacks[self.block_name] = [] self.request.callbacks[self.block_name].append(self) - - def reset (self): - ''' + def reset(self): + """ Wrap the reset routine of the internal bit_field primitive. - ''' + """ self.bit_field.reset() + + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.name) \ No newline at end of file diff --git a/sulley/constants.py b/sulley/constants.py new file mode 100644 index 0000000..b4b985f --- /dev/null +++ b/sulley/constants.py @@ -0,0 +1,2 @@ +BIG_ENDIAN = ">" +LITTLE_ENDIAN = "<" \ No newline at end of file diff --git a/sulley/fuzzers.py b/sulley/fuzzers.py new file mode 100644 index 0000000..060e474 --- /dev/null +++ b/sulley/fuzzers.py @@ -0,0 +1,30 @@ +from .sex import MustImplementException + + +class Fuzzer(object): + blocks = [] + + def __init__(self): + pass + + def send(self): + raise MustImplementException("You must implement a send() function in your fuzzer!") + + def __repr__(self): + return "" + + +class BlockBasedFuzzer(Fuzzer): + def __init__(self): + super(BlockBasedFuzzer, self).__init__() + + def __repr__(self): + return "" + + +class DumbFileFuzzer(Fuzzer): + def __init__(self): + super(DumbFileFuzzer, self).__init__() + + def __repr__(self): + return "" diff --git a/sulley/helpers.py b/sulley/helpers.py new file mode 100644 index 0000000..e183629 --- /dev/null +++ b/sulley/helpers.py @@ -0,0 +1,111 @@ +import socket +import platform +import ctypes as c +import zlib + +# noinspection PyPep8Naming +import struct +import re + + +def get_max_udp_size(): + """ + Crazy CTypes magic to do a getsockopt() which determines the max UDP payload size in a platform-agnostic way. + + @rtype: long + @return: The maximum length of a UDP packet the current platform supports + """ + windows = platform.uname()[0] == "Windows" + mac = platform.uname()[0] == "Darwin" + linux = platform.uname()[0] == "Linux" + lib = None + + if windows: + sol_socket = c.c_int(0xffff) + sol_max_msg_size = 0x2003 + lib = c.WinDLL('Ws2_32.dll') + opt = c.c_int(sol_max_msg_size) + elif linux or mac: + if mac: + lib = c.cdll.LoadLibrary('libc.dylib') + elif linux: + lib = c.cdll.LoadLibrary('libc.so.6') + sol_socket = c.c_int(socket.SOL_SOCKET) + opt = c.c_int(socket.SO_SNDBUF) + + else: + raise Exception("Unknown platform!") + + ulong_size = c.sizeof(c.c_ulong) + buf = c.create_string_buffer(ulong_size) + bufsize = c.c_int(ulong_size) + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + lib.getsockopt( + sock.fileno(), + sol_socket, + opt, + buf, + c.pointer(bufsize) + ) + + return c.c_ulong.from_buffer(buf).value + + +def calculate_four_byte_padding(string, character="\x00"): + return character * ((4 - (len(string) & 3)) & 3) + + +def crc16(string, value=0): + """ + CRC-16 poly: p(x) = x**16 + x**15 + x**2 + 1 + """ + crc16_table = [] + for byte in range(256): + crc = 0 + + for bit in range(8): + if (byte ^ crc) & 1: + crc = (crc >> 1) ^ 0xa001 # polly + else: + crc >>= 1 + + byte >>= 1 + + crc16_table.append(crc) + + for ch in string: + value = crc16_table[ord(ch) ^ (value & 0xff)] ^ (value >> 8) + + return value + + +def crc32(string): + return zlib.crc32(string) & 0xFFFFFFFF + + +def uuid_bin_to_str(uuid): + """ + Convert a binary UUID to human readable string. + """ + (block1, block2, block3) = struct.unpack("HHL", uuid[8:16]) + + return "%08x-%04x-%04x-%04x-%04x%08x" % (block1, block2, block3, block4, block5, block6) + + +def uuid_str_to_bin(uuid): + """ + Ripped from Core Impacket. Converts a UUID string to binary form. + """ + uuid_re = r'([\dA-Fa-f]{8})-([\dA-Fa-f]{4})-([\dA-Fa-f]{4})-([\dA-Fa-f]{4})-([\dA-Fa-f]{4})([\dA-Fa-f]{8})' + + matches = re.match(uuid_re, uuid) + + (uuid1, uuid2, uuid3, uuid4, uuid5, uuid6) = map(lambda x: long(x, 16), matches.groups()) + + uuid = struct.pack('HHL', uuid4, uuid5, uuid6) + + return uuid diff --git a/sulley/instrumentation.py b/sulley/instrumentation.py index 4cdd5c9..9918860 100644 --- a/sulley/instrumentation.py +++ b/sulley/instrumentation.py @@ -1,21 +1,22 @@ -class external: - ''' +class External: + """ External instrumentation class Monitor a target which doesn't support a debugger, allowing external commands to be called - ''' + """ def __init__(self, pre=None, post=None, start=None, stop=None): - ''' - @type pre: Function + """ + @type pre: def @param pre: Callback called before each test case - @type post: Function - @param post: Callback called after each test case for instrumentation. Must return True if the target is still active, False otherwise. - @type start: Function + @type post: def + @param post: Callback called after each test case for instrumentation. Must return True if the target is still + active, False otherwise. + @type start: def @param start: Callback called to start the target - @type stop: Function + @type stop: def @param stop: Callback called to stop the target - ''' + """ self.pre = pre self.post = post @@ -23,78 +24,74 @@ def __init__(self, pre=None, post=None, start=None, stop=None): self.stop = stop self.__dbg_flag = False - + # noinspection PyMethodMayBeStatic def alive(self): - ''' + """ Check if this script is alive. Always True. - ''' + """ return True - def debug(self, msg): - ''' + """ Print a debug mesage. - ''' + """ if self.__dbg_flag: print "EXT-INSTR> %s" % msg - + # noinspection PyUnusedLocal def pre_send(self, test_number): - ''' + """ This routine is called before the fuzzer transmits a test case and ensure the target is alive. @type test_number: Integer @param test_number: Test number. - ''' + """ if self.pre: self.pre() - def post_send(self): - ''' + """ This routine is called after the fuzzer transmits a test case and returns the status of the target. @rtype: Boolean @return: Return True if the target is still active, False otherwise. - ''' + """ if self.post: return self.post() else: return True - def start_target(self): - ''' + """ Start up the target. Called when post_send failed. Returns success of failure of the action If no method defined, false is returned - ''' + """ if self.start: return self.start() else: return False - def stop_target(self): - ''' + """ Stop the target. - ''' + """ if self.stop: self.stop() - + # noinspection PyMethodMayBeStatic def get_crash_synopsis(self): - ''' + """ Return the last recorded crash synopsis. @rtype: String @return: Synopsis of last recorded crash. - ''' + """ return 'External instrumentation detects a crash...\n' diff --git a/sulley/legos/__init__.py b/sulley/legos/__init__.py index 93ecee6..83b759e 100644 --- a/sulley/legos/__init__.py +++ b/sulley/legos/__init__.py @@ -4,12 +4,13 @@ import xdr # all defined legos must be added to this bin. -BIN = {} -BIN["ber_string"] = ber.string -BIN["ber_integer"] = ber.integer -BIN["dns_hostname"] = misc.dns_hostname -BIN["ndr_conformant_array"] = dcerpc.ndr_conformant_array -BIN["ndr_wstring"] = dcerpc.ndr_wstring -BIN["ndr_string"] = dcerpc.ndr_string -BIN["tag"] = misc.tag -BIN["xdr_string"] = xdr.string +BIN = { + "ber_string": ber.String, + "ber_integer": ber.Integer, + "dns_hostname": misc.DNSHostname, + "ndr_conformant_array": dcerpc.NdrConformantArray, + "ndr_wstring": dcerpc.NdrWString, + "ndr_string": dcerpc.NdrString, + "tag": misc.Tag, + "xdr_string": xdr.String +} diff --git a/sulley/legos/ber.py b/sulley/legos/ber.py index fe6e722..0100793 100644 --- a/sulley/legos/ber.py +++ b/sulley/legos/ber.py @@ -1,24 +1,26 @@ -######################################################################################################################## + ### ASN.1 / BER TYPES (http://luca.ntop.org/Teaching/Appunti/asn1.html) -######################################################################################################################## -import struct + from sulley import blocks, primitives, sex +from sulley.constants import BIG_ENDIAN -######################################################################################################################## -class string (blocks.block): - ''' +class String(blocks.Block): + """ [0x04][0x84][dword length][string] Where: 0x04 = string 0x84 = length is 4 bytes - ''' + """ + + def __init__(self, name, request, value, options=None): + if not options: + options = {} - def __init__ (self, name, request, value, options={}): - blocks.block.__init__(self, name, request, None, None, None, None) + super(String).__init__(name, request) self.value = value self.options = options @@ -27,35 +29,37 @@ def __init__ (self, name, request, value, options={}): if not self.value: raise sex.SullyRuntimeError("MISSING LEGO.ber_string DEFAULT VALUE") - str_block = blocks.block(name + "_STR", request) - str_block.push(primitives.string(self.value)) + str_block = blocks.Block(name + "_STR", request) + str_block.push(primitives.String(self.value)) - self.push(blocks.size(name + "_STR", request, endian=">", fuzzable=True)) + self.push(blocks.Size(name + "_STR", request, endian=BIG_ENDIAN, fuzzable=True)) self.push(str_block) - - def render (self): + def render(self): # let the parent do the initial render. - blocks.block.render(self) + blocks.Block.render(self) + # TODO: What is this I don't even self.rendered = self.prefix + "\x84" + self.rendered return self.rendered -######################################################################################################################## -class integer (blocks.block): - ''' +class Integer(blocks.Block): + """ [0x02][0x04][dword] Where: 0x02 = integer 0x04 = integer length is 4 bytes - ''' + """ + + def __init__(self, name, request, value, options=None): + if not options: + options = {} - def __init__ (self, name, request, value, options={}): - blocks.block.__init__(self, name, request, None, None, None, None) + super(Integer).__init__(name, request) self.value = value self.options = options @@ -63,12 +67,11 @@ def __init__ (self, name, request, value, options={}): if not self.value: raise sex.SullyRuntimeError("MISSING LEGO.ber_integer DEFAULT VALUE") - self.push(primitives.dword(self.value, endian=">")) - + self.push(primitives.DWord(self.value, endian=BIG_ENDIAN)) - def render (self): + def render(self): # let the parent do the initial render. - blocks.block.render(self) + blocks.Block.render(self) self.rendered = "\x02\x04" + self.rendered return self.rendered \ No newline at end of file diff --git a/sulley/legos/dcerpc.py b/sulley/legos/dcerpc.py index 1da63ba..0a5ef03 100644 --- a/sulley/legos/dcerpc.py +++ b/sulley/legos/dcerpc.py @@ -1,82 +1,80 @@ -######################################################################################################################## -### MSRPC NDR TYPES -######################################################################################################################## +# ## MSRPC NDR TYPES + import struct from sulley import blocks, primitives, sex +from sulley.helpers import calculate_four_byte_padding -######################################################################################################################## -def ndr_pad (string): - return "\x00" * ((4 - (len(string) & 3)) & 3) - - -######################################################################################################################## -class ndr_conformant_array (blocks.block): - ''' +class NdrConformantArray(blocks.Block): + """ Note: this is not for fuzzing the RPC protocol but rather just representing an NDR string for fuzzing the actual client. - ''' + """ - def __init__ (self, name, request, value, options={}): - blocks.block.__init__(self, name, request, None, None, None, None) + def __init__(self, name, request, value, options=None): + if not options: + options = {} - self.value = value + super(NdrConformantArray).__init__(name, request) + + self.value = value self.options = options if not self.value: raise sex.SullyRuntimeError("MISSING LEGO.ndr_conformant_array DEFAULT VALUE") - self.push(primitives.string(self.value)) - + self.push(primitives.String(self.value)) - def render (self): - ''' + def render(self): + """ We overload and extend the render routine in order to properly pad and prefix the string. [dword length][array][pad] - ''' + """ # let the parent do the initial render. - blocks.block.render(self) + blocks.Block.render(self) # encode the empty string correctly: if self.rendered == "": self.rendered = "\x00\x00\x00\x00" else: - self.rendered = struct.pack(" # [delim][string][delim] - self.push(primitives.delim("<")) - self.push(primitives.string(self.value)) - self.push(primitives.delim(">")) \ No newline at end of file + self.push(primitives.Delim("<")) + self.push(primitives.String(self.value)) + self.push(primitives.Delim(">")) \ No newline at end of file diff --git a/sulley/legos/xdr.py b/sulley/legos/xdr.py index bfee7d0..b3e2dd7 100644 --- a/sulley/legos/xdr.py +++ b/sulley/legos/xdr.py @@ -1,25 +1,23 @@ -######################################################################################################################## + ### XDR TYPES (http://www.freesoft.org/CIE/RFC/1832/index.htm) -######################################################################################################################## + import struct from sulley import blocks, primitives, sex +from sulley.helpers import calculate_four_byte_padding -######################################################################################################################## -def xdr_pad (string): - return "\x00" * ((4 - (len(string) & 3)) & 3) - - -######################################################################################################################## -class string (blocks.block): - ''' +class String (blocks.Block): + """ Note: this is not for fuzzing the XDR protocol but rather just representing an XDR string for fuzzing the actual client. - ''' + """ + + def __init__(self, name, request, value, options=None): + if not options: + options = {} - def __init__ (self, name, request, value, options={}): - blocks.block.__init__(self, name, request, None, None, None, None) + super(String).__init__(name, request) self.value = value self.options = options @@ -27,23 +25,23 @@ def __init__ (self, name, request, value, options={}): if not self.value: raise sex.SullyRuntimeError("MISSING LEGO.xdr_string DEFAULT VALUE") - self.push(primitives.string(self.value)) - + self.push(primitives.String(self.value)) - def render (self): - ''' + def render(self): + """ We overload and extend the render routine in order to properly pad and prefix the string. [dword length][array][pad] - ''' + """ # let the parent do the initial render. - blocks.block.render(self) + blocks.Block.render(self) # encode the empty string correctly: if self.rendered == "": self.rendered = "\x00\x00\x00\x00" else: - self.rendered = struct.pack(">L", len(self.rendered)) + self.rendered + xdr_pad(self.rendered) + size_header = struct.pack(">L", len(self.rendered)) + self.rendered = size_header + self.rendered + calculate_four_byte_padding(self.rendered) return self.rendered diff --git a/sulley/pedrpc.py b/sulley/pedrpc.py index e07fc79..1025b0b 100644 --- a/sulley/pedrpc.py +++ b/sulley/pedrpc.py @@ -4,9 +4,9 @@ import socket import cPickle -######################################################################################################################## -class client: - def __init__ (self, host, port): + +class Client: + def __init__(self, host, port): self.__host = host self.__port = port self.__dbg_flag = False @@ -14,30 +14,26 @@ def __init__ (self, host, port): self.__retry = 0 self.NOLINGER = struct.pack('ii', 1, 0) - - #################################################################################################################### - def __getattr__ (self, method_name): - ''' + def __getattr__(self, method_name): + """ This routine is called by default when a requested attribute (or method) is accessed that has no definition. Unfortunately __getattr__ only passes the requested method name and not the arguments. So we extend the functionality with a little lambda magic to the routine method_missing(). Which is actually how Ruby handles missing methods by default ... with arguments. Now we are just as cool as Ruby. - @type method_name: String + @type method_name: str @param method_name: The name of the requested and undefined attribute (or method in our case). - @rtype: Lambda + @rtype: lambda @return: Lambda magic passing control (and in turn the arguments we want) to self.method_missing(). - ''' + """ return lambda *args, **kwargs: self.__method_missing(method_name, *args, **kwargs) - - #################################################################################################################### - def __connect (self): - ''' + def __connect(self): + """ Connect to the PED-RPC server. - ''' + """ # if we have a pre-existing server socket, ensure it's closed. self.__disconnect() @@ -59,41 +55,35 @@ def __connect (self): self.__server_sock.settimeout(None) self.__server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, self.NOLINGER) - - #################################################################################################################### - def __disconnect (self): - ''' + def __disconnect(self): + """ Ensure the socket is torn down. - ''' + """ - if self.__server_sock != None: + if self.__server_sock is not None: self.__debug("closing server socket") self.__server_sock.close() self.__server_sock = None - - #################################################################################################################### - def __debug (self, msg): + def __debug(self, msg): if self.__dbg_flag: print "PED-RPC> %s" % msg - - #################################################################################################################### - def __method_missing (self, method_name, *args, **kwargs): - ''' + def __method_missing(self, method_name, *args, **kwargs): + """ See the notes for __getattr__ for related notes. This method is called, in the Ruby fashion, with the method name and arguments for any requested but undefined class method. - @type method_name: String + @type method_name: str @param method_name: The name of the requested and undefined attribute (or method in our case). - @type *args: Tuple + @type *args: tuple @param *args: Tuple of arguments. - @type **kwargs Dictionary + @type **kwargs dict @param **kwargs: Dictioanry of arguments. @rtype: Mixed @return: Return value of the mirrored method. - ''' + """ # return a value so lines of code like the following work: # x = pedrpc.client(host, port) @@ -125,10 +115,8 @@ def __method_missing (self, method_name, *args, **kwargs): self.__disconnect() return ret - - #################################################################################################################### - def __pickle_recv (self): - ''' + def __pickle_recv(self): + """ This routine is used for marshaling arbitrary data from the PyDbg server. We can send pretty much anything here. For example a tuple containing integers, strings, arbitrary objects and structures. Our "protocol" is a simple length-value protocol where each datagram is prefixed by a 4-byte length of the data to be received. @@ -136,7 +124,7 @@ def __pickle_recv (self): @raise pdx: An exception is raised if the connection was severed. @rtype: Mixed @return: Whatever is received over the socket. - ''' + """ try: # TODO: this should NEVER fail, but alas, it does and for the time being i can't figure out why. @@ -159,10 +147,8 @@ def __pickle_recv (self): return cPickle.loads(received) - - #################################################################################################################### - def __pickle_send (self, data): - ''' + def __pickle_send(self, data): + """ This routine is used for marshaling arbitrary data to the PyDbg server. We can send pretty much anything here. For example a tuple containing integers, strings, arbitrary objects and structures. Our "protocol" is a simple length-value protocol where each datagram is prefixed by a 4-byte length of the data to be received. @@ -171,7 +157,7 @@ def __pickle_send (self, data): @param data: Data to marshal and transmit. Data can *pretty much* contain anything you throw at it. @raise pdx: An exception is raised if the connection was severed. - ''' + """ data = cPickle.dumps(data, protocol=2) self.__debug("sending %d bytes" % len(data)) @@ -184,9 +170,8 @@ def __pickle_send (self, data): raise Exception -######################################################################################################################## -class server: - def __init__ (self, host, port): +class Server: + def __init__(self, host, port): self.__host = host self.__port = port self.__dbg_flag = False @@ -203,28 +188,22 @@ def __init__ (self, host, port): sys.stderr.write("unable to bind to %s:%d\n" % (host, port)) sys.exit(1) - - #################################################################################################################### - def __disconnect (self): - ''' + def __disconnect(self): + """ Ensure the socket is torn down. - ''' + """ - if self.__client_sock != None: + if self.__client_sock is not None: self.__debug("closing client socket") self.__client_sock.close() self.__client_sock = None - - #################################################################################################################### - def __debug (self, msg): + def __debug(self, msg): if self.__dbg_flag: print "PED-RPC> %s" % msg - - #################################################################################################################### - def __pickle_recv (self): - ''' + def __pickle_recv(self): + """ This routine is used for marshaling arbitrary data from the PyDbg server. We can send pretty much anything here. For example a tuple containing integers, strings, arbitrary objects and structures. Our "protocol" is a simple length-value protocol where each datagram is prefixed by a 4-byte length of the data to be received. @@ -232,7 +211,7 @@ def __pickle_recv (self): @raise pdx: An exception is raised if the connection was severed. @rtype: Mixed @return: Whatever is received over the socket. - ''' + """ try: length = struct.unpack(" connection to client severed during send()\n") raise Exception - - #################################################################################################################### - def serve_forever (self): + def serve_forever(self): self.__debug("serving up a storm") while 1: @@ -295,7 +270,9 @@ def serve_forever (self): try: # resolve a pointer to the requested method and call it. + # Wat. exec("method_pointer = self.%s" % method_name) + # noinspection PyUnresolvedReferences ret = method_pointer(*args, **kwargs) except AttributeError: # if the method can't be found notify the user and raise an error diff --git a/sulley/pgraph/__init__.py b/sulley/pgraph/__init__.py index 95c25fd..847b3e7 100644 --- a/sulley/pgraph/__init__.py +++ b/sulley/pgraph/__init__.py @@ -13,14 +13,7 @@ # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -''' -@author: Pedram Amini -@license: GNU General Public License 2.0 or later -@contact: pedram.amini@gmail.com -@organization: www.openrce.org -''' - from cluster import * -from edge import * -from graph import * -from node import * \ No newline at end of file +from edge import * +from graph import * +from node import * \ No newline at end of file diff --git a/sulley/pgraph/cluster.py b/sulley/pgraph/cluster.py index 0f1093e..048d81b 100644 --- a/sulley/pgraph/cluster.py +++ b/sulley/pgraph/cluster.py @@ -13,54 +13,39 @@ # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -''' -@author: Pedram Amini -@license: GNU General Public License 2.0 or later -@contact: pedram.amini@gmail.com -@organization: www.openrce.org -''' -import node - -class cluster (object): - ''' - ''' +class Cluster(object): id = None nodes = [] - #################################################################################################################### - def __init__ (self, id=None): - ''' + def __init__(self, cluster_id=None): + """ Class constructor. - ''' + """ - self.id = id + self.id = cluster_id self.nodes = [] - - #################################################################################################################### - def add_node (self, node): - ''' + def add_node(self, node): + """ Add a node to the cluster. @type node: pGRAPH Node @param node: Node to add to cluster - ''' + """ self.nodes.append(node) return self - - #################################################################################################################### - def del_node (self, node_id): - ''' + def del_node(self, node_id): + """ Remove a node from the cluster. - @type node: pGRAPH Node - @param node: Node to remove from cluster - ''' + @type node_id: pGRAPH Node + @param node_id: Node to remove from cluster + """ for node in self.nodes: if node.id == node_id: @@ -69,20 +54,18 @@ def del_node (self, node_id): return self - - #################################################################################################################### - def find_node (self, attribute, value): - ''' + def find_node(self, attribute, value): + """ Find and return the node with the specified attribute / value pair. - @type attribute: String + @type attribute: str @param attribute: Attribute name we are looking for @type value: Mixed @param value: Value of attribute we are looking for @rtype: Mixed @return: Node, if attribute / value pair is matched. None otherwise. - ''' + """ for node in self.nodes: if hasattr(node, attribute): @@ -91,7 +74,5 @@ def find_node (self, attribute, value): return None - - #################################################################################################################### - def render (self): + def render(self): pass \ No newline at end of file diff --git a/sulley/pgraph/edge.py b/sulley/pgraph/edge.py index 74b1c83..effc399 100644 --- a/sulley/pgraph/edge.py +++ b/sulley/pgraph/edge.py @@ -13,44 +13,36 @@ # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -''' -@author: Pedram Amini -@license: GNU General Public License 2.0 or later -@contact: pedram.amini@gmail.com -@organization: www.openrce.org -''' +import pydot -class edge (object): - ''' - ''' - id = None - src = None - dst = None +class Edge(object): + id = None + src = None + dst = None # general graph attributes. color = 0x000000 label = "" # gml relevant attributes. - gml_arrow = "none" - gml_stipple = 1 - gml_line_width = 1.0 + gml_arrow = "none" + gml_stipple = 1 + gml_line_width = 1.0 - #################################################################################################################### - def __init__ (self, src, dst): - ''' + def __init__(self, src, dst): + """ Class constructor. @type src: Mixed @param src: Edge source @type dst: Mixed @param dst: Edge destination - ''' + """ # the unique id for any edge (provided that duplicates are not allowed) is the combination of the source and # the destination stored as a long long. - self.id = (src << 32) + dst + self.id = (src << 32) + dst self.src = src self.dst = dst @@ -59,22 +51,20 @@ def __init__ (self, src, dst): self.label = "" # gml relevant attributes. - self.gml_arrow = "none" - self.gml_stipple = 1 - self.gml_line_width = 1.0 + self.gml_arrow = "none" + self.gml_stipple = 1 + self.gml_line_width = 1.0 - - #################################################################################################################### - def render_edge_gml (self, graph): - ''' + def render_edge_gml(self, graph): + """ Render an edge description suitable for use in a GML file using the set internal attributes. - @type graph: pgraph.graph + @type graph: pgraph.Graph @param graph: Top level graph object containing the current edge @rtype: String @return: GML edge description - ''' + """ src = graph.find_node("id", self.src) dst = graph.find_node("id", self.dst) @@ -83,35 +73,37 @@ def render_edge_gml (self, graph): if not src or not dst: return "" - edge = ' edge [\n' - edge += ' source %d\n' % src.number - edge += ' target %d\n' % dst.number - edge += ' generalization 0\n' - edge += ' graphics [\n' - edge += ' type "line"\n' - edge += ' arrow "%s"\n' % self.gml_arrow - edge += ' stipple %d\n' % self.gml_stipple - edge += ' lineWidth %f\n' % self.gml_line_width - edge += ' fill "#%06x"\n' % self.color - edge += ' ]\n' - edge += ' ]\n' + edge = """ + edge [ + source %(srcNumber)d + target %(dstNumber)d + generalization 0 + graphics [ + type "line" + arrow "%(gml_arrow)s" + stripple %(gml_stipple)d + linWidth %(gml_line_width)f + fill "#%(color)06x" + ] + ] + """ % { + "color": self.color, + "srcNumber": src.number, + "dstNumber": dst.number, + "gml_arrow": self.gml_arrow, + "gml_stipple": self.gml_stipple, + "gml_line_width": self.gml_line_width + } return edge - - #################################################################################################################### - def render_edge_graphviz (self, graph): - ''' + def render_edge_graphviz(self): + """ Render an edge suitable for use in a Pydot graph using the set internal attributes. - @type graph: pgraph.graph - @param graph: Top level graph object containing the current edge - @rtype: pydot.Edge() @return: Pydot object representing edge - ''' - - import pydot + """ # no need to validate if nodes exist for src/dst. graphviz takes care of that for us transparently. @@ -124,18 +116,16 @@ def render_edge_graphviz (self, graph): return dot_edge - - #################################################################################################################### - def render_edge_udraw (self, graph): - ''' + def render_edge_udraw(self, graph): + """ Render an edge description suitable for use in a GML file using the set internal attributes. - @type graph: pgraph.graph + @type graph: pgraph.Graph @param graph: Top level graph object containing the current edge @rtype: String @return: GML edge description - ''' + """ src = graph.find_node("id", self.src) dst = graph.find_node("id", self.dst) @@ -147,37 +137,50 @@ def render_edge_udraw (self, graph): # translate newlines for uDraw. self.label = self.label.replace("\n", "\\n") - udraw = 'l("%08x->%08x",' % (self.src, self.dst) - udraw += 'e("",' # open edge - udraw += '[' # open attributes - udraw += 'a("EDGECOLOR","#%06x"),' % self.color - udraw += 'a("OBJECT","%s")' % self.label - udraw += '],' # close attributes - udraw += 'r("%08x")' % self.dst - udraw += ')' # close edge - udraw += ')' # close element + udraw = """ + l("%(src)08x->%(dst)08x", + e("", + [ + a("EDGECOLOR","#%(color)06x"), + a("OBJECT","%(label)s") + ], + r("%(dst)08x") + ) + ) + """ % { + "src": self.src, + "dst": self.dst, + "color": self.color, + "label": self.label, + + } return udraw - - #################################################################################################################### - def render_edge_udraw_update (self): - ''' + def render_edge_udraw_update(self): + """ Render an edge update description suitable for use in a GML file using the set internal attributes. @rtype: String @return: GML edge update description - ''' + """ # translate newlines for uDraw. self.label = self.label.replace("\n", "\\n") - udraw = 'new_edge("%08x->%08x","",' % (self.src, self.dst) - udraw += '[' - udraw += 'a("EDGECOLOR","#%06x"),' % self.color - udraw += 'a("OBJECT","%s")' % self.label - udraw += '],' - udraw += '"%08x","%08x"' % (self.src, self.dst) - udraw += ')' + udraw = """ + new_edge("%(src)08x->%(dst)08x","", + [ + a("EDGECOLOR","#%(color)06x"), + a("OBJECT","%(label)s") + ] + "%(src)08x","%(dst)08x" + ) + """ % { + "src": self.src, + "dst": self.dst, + "color": self.color, + "label": self.label, + } return udraw \ No newline at end of file diff --git a/sulley/pgraph/graph.py b/sulley/pgraph/graph.py index f016cf0..df1eddf 100644 --- a/sulley/pgraph/graph.py +++ b/sulley/pgraph/graph.py @@ -13,129 +13,104 @@ # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -''' -@author: Pedram Amini -@license: GNU General Public License 2.0 or later -@contact: pedram.amini@gmail.com -@organization: www.openrce.org -''' - -import node -import edge -import cluster - +import pydot import copy -class graph (object): - ''' + +class Graph(object): + """ @todo: Add support for clusters @todo: Potentially swap node list with a node dictionary for increased performance - ''' + """ id = None clusters = [] edges = {} nodes = {} - #################################################################################################################### - def __init__ (self, id=None): - ''' - ''' - - self.id = id + def __init__(self, graph_id=None): + self.id = graph_id self.clusters = [] self.edges = {} self.nodes = {} - - #################################################################################################################### - def add_cluster (self, cluster): - ''' + def add_cluster(self, cluster): + """ Add a pgraph cluster to the graph. @type cluster: pGRAPH Cluster @param cluster: Cluster to add to graph - ''' + """ self.clusters.append(cluster) return self - - #################################################################################################################### - def add_edge (self, edge, prevent_dups=True): - ''' + def add_edge(self, graph_edge, prevent_dups=True): + """ Add a pgraph edge to the graph. Ensures a node exists for both the source and destination of the edge. - @type edge: pGRAPH Edge - @param edge: Edge to add to graph + @type graph_edge: pGRAPH Edge + @param graph_edge: Edge to add to graph @type prevent_dups: Boolean @param prevent_dups: (Optional, Def=True) Flag controlling whether or not the addition of duplicate edges is ok - ''' + """ if prevent_dups: - if self.edges.has_key(edge.id): + if graph_edge.id in self.edges: return self # ensure the source and destination nodes exist. - if self.find_node("id", edge.src) and self.find_node("id", edge.dst): - self.edges[edge.id] = edge + if self.find_node("id", graph_edge.src) and self.find_node("id", graph_edge.dst): + self.edges[graph_edge.id] = graph_edge return self - - #################################################################################################################### - def add_graph (self, other_graph): - ''' + def add_graph(self, other_graph): + """ Alias of graph_cat(). Concatenate the other graph into the current one. @todo: Add support for clusters @see: graph_cat() - @type other_graph: pgraph.graph + @type other_graph: pgraph.Graph @param other_graph: Graph to concatenate into this one. - ''' + """ return self.graph_cat(other_graph) - - #################################################################################################################### - def add_node (self, node): - ''' + def add_node(self, node): + """ Add a pgraph node to the graph. Ensures a node with the same id does not already exist in the graph. @type node: pGRAPH Node @param node: Node to add to graph - ''' + """ node.number = len(self.nodes) - if not self.nodes.has_key(node.id): + if not node.id in self.nodes: self.nodes[node.id] = node return self - - #################################################################################################################### - def del_cluster (self, id): - ''' + def del_cluster(self, cluster_id): + """ Remove a cluster from the graph. - @type id: Mixed - @param id: Identifier of cluster to remove from graph - ''' + @type cluster_id: Mixed + @param cluster_id: Identifier of cluster to remove from graph + """ for cluster in self.clusters: - if cluster.id == id: + if cluster.id == cluster_id: self.clusters.remove(cluster) break return self - - #################################################################################################################### - def del_edge (self, id=None, src=None, dst=None): - ''' + def del_edge(self, graph_id=None, src=None, dst=None): + """ Remove an edge from the graph. There are two ways to call this routine, with an edge id:: graph.del_edge(id) @@ -144,97 +119,87 @@ def del_edge (self, id=None, src=None, dst=None): graph.del_edge(src=source, dst=destination) - @type id: Mixed - @param id: (Optional) Identifier of edge to remove from graph - @type src: Mixed - @param src: (Optional) Source of edge to remove from graph - @type dst: Mixed - @param dst: (Optional) Destination of edge to remove from graph - ''' + @type graph_id: Mixed + @param graph_id: (Optional) Identifier of edge to remove from graph + @type src: Mixed + @param src: (Optional) Source of edge to remove from graph + @type dst: Mixed + @param dst: (Optional) Destination of edge to remove from graph + """ - if not id: - id = (src << 32) + dst + if not graph_id: + graph_id = (src << 32) + dst - if self.edges.has_key(id): - del self.edges[id] + if graph_id in self.edges: + del self.edges[graph_id] return self - - #################################################################################################################### - def del_graph (self, other_graph): - ''' + def del_graph(self, other_graph): + """ Alias of graph_sub(). Remove the elements shared between the current graph and other graph from the current graph. @todo: Add support for clusters @see: graph_sub() - @type other_graph: pgraph.graph + @type other_graph: pgraph.Graph @param other_graph: Graph to diff/remove against - ''' + """ return self.graph_sub(other_graph) - - #################################################################################################################### - def del_node (self, id): - ''' + def del_node(self, node_id): + """ Remove a node from the graph. @type node_id: Mixed @param node_id: Identifier of node to remove from graph - ''' + """ - if self.nodes.has_key(id): - del self.nodes[id] + if node_id in self.nodes: + del self.nodes[node_id] return self - - #################################################################################################################### - def edges_from (self, id): - ''' + def edges_from(self, edge_id): + """ Enumerate the edges from the specified node. - @type id: Mixed - @param id: Identifier of node to enumerate edges from + @type edge_id: Mixed + @param edge_id: Identifier of node to enumerate edges from - @rtype: List + @rtype: list @return: List of edges from the specified node - ''' - - return [edge for edge in self.edges.values() if edge.src == id] + """ + return [edge_value for edge_value in self.edges.values() if edge_value.src == edge_id] - #################################################################################################################### - def edges_to (self, id): - ''' + def edges_to(self, edge_id): + """ Enumerate the edges to the specified node. - @type id: Mixed - @param id: Identifier of node to enumerate edges to + @type edge_id: Mixed + @param edge_id: Identifier of node to enumerate edges to - @rtype: List + @rtype: list @return: List of edges to the specified node - ''' - - return [edge for edge in self.edges.values() if edge.dst == id] + """ + return [edge_value for edge_value in self.edges.values() if edge_value.dst == edge_id] - #################################################################################################################### - def find_cluster (self, attribute, value): - ''' + def find_cluster(self, attribute, value): + """ Find and return the cluster with the specified attribute / value pair. - @type attribute: String + @type attribute: str @param attribute: Attribute name we are looking for @type value: Mixed @param value: Value of attribute we are looking for @rtype: Mixed @return: Cluster, if attribute / value pair is matched. None otherwise. - ''' + """ for cluster in self.clusters: if hasattr(cluster, attribute): @@ -243,20 +208,18 @@ def find_cluster (self, attribute, value): return None - - #################################################################################################################### - def find_cluster_by_node (self, attribute, value): - ''' + def find_cluster_by_node(self, attribute, value): + """ Find and return the cluster that contains the node with the specified attribute / value pair. - @type attribute: String + @type attribute: str @param attribute: Attribute name we are looking for @type value: Mixed @param value: Value of attribute we are looking for @rtype: Mixed @return: Cluster, if node with attribute / value pair is matched. None otherwise. - ''' + """ for cluster in self.clusters: for node in cluster: @@ -266,51 +229,48 @@ def find_cluster_by_node (self, attribute, value): return None - - #################################################################################################################### - def find_edge (self, attribute, value): - ''' + def find_edge(self, attribute, value): + """ Find and return the edge with the specified attribute / value pair. - @type attribute: String + @type attribute: str @param attribute: Attribute name we are looking for @type value: Mixed @param value: Value of attribute we are looking for @rtype: Mixed @return: Edge, if attribute / value pair is matched. None otherwise. - ''' + """ # if the attribute to search for is the id, simply return the edge from the internal hash. - if attribute == "id" and self.edges.has_key(value): + if attribute == "id" and value in self.edges: return self.edges[value] # step through all the edges looking for the given attribute/value pair. else: - for edges in self.edges.values(): - if hasattr(edge, attribute): - if getattr(edge, attribute) == value: - return edge + #TODO: Verify that this actually works? Was broken when I got here ;-P + for node_edge in self.edges.values(): + if hasattr(node_edge, attribute): + if getattr(node_edge, attribute) == value: + return node_edge return None - - #################################################################################################################### - def find_node (self, attribute, value): - ''' + def find_node(self, attribute, value): + """ Find and return the node with the specified attribute / value pair. - @type attribute: String + @type attribute: str @param attribute: Attribute name we are looking for - @type value: Mixed + @type value: mixed @param value: Value of attribute we are looking for @rtype: Mixed @return: Node, if attribute / value pair is matched. None otherwise. - ''' + """ # if the attribute to search for is the id, simply return the node from the internal hash. - if attribute == "id" and self.nodes.has_key(value): + if attribute == "id" and value in self.nodes: return self.nodes[value] # step through all the nodes looking for the given attribute/value pair. @@ -322,17 +282,15 @@ def find_node (self, attribute, value): return None - - #################################################################################################################### - def graph_cat (self, other_graph): - ''' + def graph_cat(self, other_graph): + """ Concatenate the other graph into the current one. @todo: Add support for clusters - @type other_graph: pgraph.graph + @type other_graph: pgraph.Graph @param other_graph: Graph to concatenate into this one. - ''' + """ for other_node in other_graph.nodes.values(): self.add_node(other_node) @@ -342,10 +300,8 @@ def graph_cat (self, other_graph): return self - - #################################################################################################################### - def graph_down (self, from_node_id, max_depth=-1): - ''' + def graph_down(self, from_node_id, max_depth=-1): + """ Create a new graph, looking down, from the specified node id to the specified depth. @type from_node_id: pgraph.node @@ -353,11 +309,11 @@ def graph_down (self, from_node_id, max_depth=-1): @type max_depth: Integer @param max_depth: (Optional, Def=-1) Number of levels to include in down graph (-1 for infinite) - @rtype: pgraph.graph + @rtype: pgraph.Graph @return: Down graph around specified node. - ''' + """ - down_graph = graph() + down_graph = Graph() from_node = self.find_node("id", from_node_id) if not from_node: @@ -372,6 +328,7 @@ def graph_down (self, from_node_id, max_depth=-1): for level in levels_to_process: next_level = [] + # noinspection PyChainedComparisons if current_depth > max_depth and max_depth != -1: break @@ -394,17 +351,15 @@ def graph_down (self, from_node_id, max_depth=-1): return down_graph - - #################################################################################################################### - def graph_intersect (self, other_graph): - ''' + def graph_intersect(self, other_graph): + """ Remove all elements from the current graph that do not exist in the other graph. @todo: Add support for clusters - @type other_graph: pgraph.graph + @type other_graph: pgraph.Graph @param other_graph: Graph to intersect with - ''' + """ for node in self.nodes.values(): if not other_graph.find_node("id", node.id): @@ -416,10 +371,8 @@ def graph_intersect (self, other_graph): return self - - #################################################################################################################### - def graph_proximity (self, center_node_id, max_depth_up=2, max_depth_down=2): - ''' + def graph_proximity(self, center_node_id, max_depth_up=2, max_depth_down=2): + """ Create a proximity graph centered around the specified node. @type center_node_id: pgraph.node @@ -429,27 +382,25 @@ def graph_proximity (self, center_node_id, max_depth_up=2, max_depth_down=2): @type max_depth_down: Integer @param max_depth_down: (Optional, Def=2) Number of downward levels to include in proximity graph - @rtype: pgraph.graph + @rtype: pgraph.Graph @return: Proximity graph around specified node. - ''' + """ prox_graph = self.graph_down(center_node_id, max_depth_down) prox_graph.add_graph(self.graph_up(center_node_id, max_depth_up)) return prox_graph - - #################################################################################################################### - def graph_sub (self, other_graph): - ''' + def graph_sub(self, other_graph): + """ Remove the elements shared between the current graph and other graph from the current graph. @todo: Add support for clusters - @type other_graph: pgraph.graph + @type other_graph: pgraph.Graph @param other_graph: Graph to diff/remove against - ''' + """ for other_node in other_graph.nodes.values(): self.del_node(other_node.id) @@ -459,10 +410,8 @@ def graph_sub (self, other_graph): return self - - #################################################################################################################### - def graph_up (self, from_node_id, max_depth=-1): - ''' + def graph_up(self, from_node_id, max_depth=-1): + """ Create a new graph, looking up, from the specified node id to the specified depth. @type from_node_id: pgraph.node @@ -470,11 +419,11 @@ def graph_up (self, from_node_id, max_depth=-1): @type max_depth: Integer @param max_depth: (Optional, Def=-1) Number of levels to include in up graph (-1 for infinite) - @rtype: pgraph.graph + @rtype: pgraph.Graph @return: Up graph to the specified node. - ''' + """ - up_graph = graph() + up_graph = Graph() from_node = self.find_node("id", from_node_id) levels_to_process = [] @@ -485,6 +434,7 @@ def graph_up (self, from_node_id, max_depth=-1): for level in levels_to_process: next_level = [] + # noinspection PyChainedComparisons if current_depth > max_depth and max_depth != -1: break @@ -507,15 +457,13 @@ def graph_up (self, from_node_id, max_depth=-1): return up_graph - - #################################################################################################################### - def render_graph_gml (self): - ''' + def render_graph_gml(self): + """ Render the GML graph description. @rtype: String @return: GML graph description. - ''' + """ gml = 'Creator "pGRAPH - Pedram Amini "\n' gml += 'directed 1\n' @@ -556,18 +504,13 @@ def render_graph_gml (self): return gml - - #################################################################################################################### - def render_graph_graphviz (self): - ''' + def render_graph_graphviz(self): + """ Render the graphviz graph structure. @rtype: pydot.Dot @return: Pydot object representing entire graph - ''' - - import pydot - + """ dot_graph = pydot.Dot() for node in self.nodes.values(): @@ -578,15 +521,13 @@ def render_graph_graphviz (self): return dot_graph - - #################################################################################################################### - def render_graph_udraw (self): - ''' + def render_graph_udraw(self): + """ Render the uDraw graph description. - @rtype: String + @rtype: str @return: uDraw graph description. - ''' + """ udraw = '[' @@ -601,15 +542,13 @@ def render_graph_udraw (self): return udraw - - #################################################################################################################### - def render_graph_udraw_update (self): - ''' + def render_graph_udraw_update(self): + """ Render the uDraw graph update description. @rtype: String @return: uDraw graph description. - ''' + """ udraw = '[' @@ -626,20 +565,18 @@ def render_graph_udraw_update (self): return udraw - - #################################################################################################################### - def update_node_id (self, current_id, new_id): - ''' + def update_node_id(self, current_id, new_id): + """ Simply updating the id attribute of a node will sever the edges to / from the given node. This routine will correctly update the edges as well. - @type current_id: Long + @type current_id: long @param current_id: Current ID of node whose ID we want to update - @type new_id: Long + @type new_id: long @param new_id: New ID to update to. - ''' + """ - if not self.nodes.has_key(current_id): + if not current_id in self.nodes: return # update the node. @@ -661,15 +598,13 @@ def update_node_id (self, current_id, new_id): self.edges[edge.id] = edge - - #################################################################################################################### - def sorted_nodes (self): - ''' + def sorted_nodes(self): + """ Return a list of the nodes within the graph, sorted by id. @rtype: List @return: List of nodes, sorted by id. - ''' + """ node_keys = self.nodes.keys() node_keys.sort() diff --git a/sulley/pgraph/node.py b/sulley/pgraph/node.py index c15e54e..a6e6994 100644 --- a/sulley/pgraph/node.py +++ b/sulley/pgraph/node.py @@ -13,90 +13,74 @@ # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -''' -@author: Pedram Amini -@license: GNU General Public License 2.0 or later -@contact: pedram.amini@gmail.com -@organization: www.openrce.org -''' - -class node (object): - ''' - ''' - - id = 0 +import pydot + + +class Node(object): + id = 0 number = 0 # general graph attributes - color = 0xEEF7FF + color = 0xEEF7FF border_color = 0xEEEEEE - label = "" - shape = "box" + label = "" + shape = "box" # gml relevant attributes. - gml_width = 0.0 - gml_height = 0.0 - gml_pattern = "1" - gml_stipple = 1 - gml_line_width = 1.0 - gml_type = "rectangle" + gml_width = 0.0 + gml_height = 0.0 + gml_pattern = "1" + gml_stipple = 1 + gml_line_width = 1.0 + gml_type = "rectangle" gml_width_shape = 1.0 # udraw relevant attributes. - udraw_image = None - udraw_info = "" - - #################################################################################################################### - def __init__ (self, id=None): - ''' - ''' + udraw_image = None + udraw_info = "" - self.id = id + def __init__(self, node_id=None): + self.id = node_id self.number = 0 # general graph attributes - self.color = 0xEEF7FF + self.color = 0xEEF7FF self.border_color = 0xEEEEEE - self.label = "" - self.shape = "box" + self.label = "" + self.shape = "box" # gml relevant attributes. - self.gml_width = 0.0 - self.gml_height = 0.0 - self.gml_pattern = "1" - self.gml_stipple = 1 - self.gml_line_width = 1.0 - self.gml_type = "rectangle" + self.gml_width = 0.0 + self.gml_height = 0.0 + self.gml_pattern = "1" + self.gml_stipple = 1 + self.gml_line_width = 1.0 + self.gml_type = "rectangle" self.gml_width_shape = 1.0 - - #################################################################################################################### - def render_node_gml (self, graph): - ''' + def render_node_gml(self): + """ Render a node description suitable for use in a GML file using the set internal attributes. - @type graph: pgraph.graph - @param graph: Top level graph object containing the current node - @rtype: String @return: GML node description. - ''' + """ # GDE does not like lines longer then approx 250 bytes. within their their own GML files you won't find lines # longer then approx 210 bytes. wo we are forced to break long lines into chunks. chunked_label = "" - cursor = 0 + cursor = 0 while cursor < len(self.label): amount = 200 # if the end of the current chunk contains a backslash or double-quote, back off some. if cursor + amount < len(self.label): - while self.label[cursor+amount] == '\\' or self.label[cursor+amount] == '"': + while self.label[cursor + amount] == '\\' or self.label[cursor + amount] == '"': amount -= 1 - chunked_label += self.label[cursor:cursor+amount] + "\\\n" - cursor += amount + chunked_label += self.label[cursor:cursor + amount] + "\\\n" + cursor += amount # if node width and height were not explicitly specified, make a best effort guess to create something nice. if not self.gml_width: @@ -106,87 +90,100 @@ def render_node_gml (self, graph): self.gml_height = len(self.label.split()) * 20 # construct the node definition. - node = ' node [\n' - node += ' id %d\n' % self.number - node += ' template "oreas:std:rect"\n' - node += ' label "' - node += '\\\n' % self.id - node += chunked_label + '"\n' - node += ' graphics [\n' - node += ' w %f\n' % self.gml_width - node += ' h %f\n' % self.gml_height - node += ' fill "#%06x"\n' % self.color - node += ' line "#%06x"\n' % self.border_color - node += ' pattern "%s"\n' % self.gml_pattern - node += ' stipple %d\n' % self.gml_stipple - node += ' lineWidth %f\n' % self.gml_line_width - node += ' type "%s"\n' % self.gml_type - node += ' width %f\n' % self.gml_width_shape - node += ' ]\n' - node += ' ]\n' + node = """ + node [ + id %(number)d + template "oreas:std:rect" + label " %(chunked_label)s" + graphics [ + w %(gml_width)f + h %(gml_height)f + fill "#%(color)06x" + line "#%(border_color)06x" + pattern "%(gml_pattern)s" + stipple %(gml_stipple)d + lineWidth %(gml_line_width)f + type "%(gml_type)s" + width %(gml_width_shape)f + ] + ] + """ % { + "number": self.number, + "id": self.id, + "gml_width": self.gml_width, + "gml_height": self.gml_height, + "color": self.color, + "border_color": self.border_color, + "gml_pattern": self.gml_pattern, + "gml_stipple": self.gml_stipple, + "gml_line_width": self.gml_line_width, + "gml_type": self.gml_type, + "gml_width_shape": self.gml_width_shape, + "chunked_label": chunked_label + } return node - - #################################################################################################################### - def render_node_graphviz (self, graph): - ''' + def render_node_graphviz(self): + """ Render a node suitable for use in a Pydot graph using the set internal attributes. - @type graph: pgraph.graph - @param graph: Top level graph object containing the current node - @rtype: pydot.Node @return: Pydot object representing node - ''' - - import pydot + """ dot_node = pydot.Node(self.id) - dot_node.label = '<%s>' % self.label.rstrip("\r\n") - dot_node.label = dot_node.label.replace("\\n", '
') - dot_node.shape = self.shape - dot_node.color = "#%06x" % self.color + dot_node.label = '<%s>' % self.label.rstrip("\r\n") + dot_node.label = dot_node.label.replace("\\n", '
') + dot_node.shape = self.shape + dot_node.color = "#%06x" % self.color dot_node.fillcolor = "#%06x" % self.color return dot_node - - #################################################################################################################### - def render_node_udraw (self, graph): - ''' + def render_node_udraw(self, graph): + """ Render a node description suitable for use in a uDraw file using the set internal attributes. - @type graph: pgraph.graph + @type graph: pgraph.Graph @param graph: Top level graph object containing the current node @rtype: String @return: uDraw node description. - ''' + """ # translate newlines for uDraw. self.label = self.label.replace("\n", "\\n") # if an image was specified for this node, update the shape and include the image tag. if self.udraw_image: - self.shape = "image" + self.shape = "image" udraw_image = 'a("IMAGE","%s"),' % self.udraw_image else: udraw_image = "" - udraw = 'l("%08x",' % self.id - udraw += 'n("",' # open node - udraw += '[' # open attributes - udraw += udraw_image - udraw += 'a("_GO","%s"),' % self.shape - udraw += 'a("COLOR","#%06x"),' % self.color - udraw += 'a("OBJECT","%s"),' % self.label - udraw += 'a("FONTFAMILY","courier"),' - udraw += 'a("INFO","%s"),' % self.udraw_info - udraw += 'a("BORDER","none")' - udraw += '],' # close attributes - udraw += '[' # open edges + udraw = """ + l("%(id)08x", + n("", + [ + %(udraw_image)s + a("_GO","%(shape)s"), + a("COLOR","#%(color)06x"), + a("OBJECT","%(label)s"), + a("FONTFAMILY","courier"), + a("INFO","%(udraw_info)s"), + a("BORDER","none") + ] + [ + """ % { + "id": self.id, + "udraw_image": udraw_image, + "shape": self.shape, + "color": self.color, + "label": self.label, + "udraw_info": self.udraw_info + } edges = graph.edges_from(self.id) @@ -201,36 +198,43 @@ def render_node_udraw (self, graph): return udraw - - #################################################################################################################### - def render_node_udraw_update (self): - ''' + def render_node_udraw_update(self): + """ Render a node update description suitable for use in a uDraw file using the set internal attributes. @rtype: String @return: uDraw node update description. - ''' + """ # translate newlines for uDraw. self.label = self.label.replace("\n", "\\n") # if an image was specified for this node, update the shape and include the image tag. if self.udraw_image: - self.shape = "image" + self.shape = "image" udraw_image = 'a("IMAGE","%s"),' % self.udraw_image else: udraw_image = "" - udraw = 'new_node("%08x","",' % self.id - udraw += '[' - udraw += udraw_image - udraw += 'a("_GO","%s"),' % self.shape - udraw += 'a("COLOR","#%06x"),' % self.color - udraw += 'a("OBJECT","%s"),' % self.label - udraw += 'a("FONTFAMILY","courier"),' - udraw += 'a("INFO","%s"),' % self.udraw_info - udraw += 'a("BORDER","none")' - udraw += ']' - udraw += ')' + udraw = """ + new_node("%(id)08x","", + [ + %(udraw_image)s + a("_GO","%(shape)s"), + a("COLOR","#%(color)06x"), + a("OBJECT","%(label)s"), + a("FONTFAMILY","courier"), + a("INFO","%(udraw_info)s"), + a("BORDER","none") + ] + ) + """ % { + "id": self.id, + "udraw_image": udraw_image, + "shape": self.shape, + "color": self.color, + "label": self.label, + "udraw_info": self.udraw_info + } return udraw \ No newline at end of file diff --git a/sulley/primitives.py b/sulley/primitives.py index 38a5e64..1ec9f84 100644 --- a/sulley/primitives.py +++ b/sulley/primitives.py @@ -1,46 +1,48 @@ import random import struct -######################################################################################################################## -class base_primitive (object): - ''' - The primitive base class implements common functionality shared across most primitives. - ''' +from constants import LITTLE_ENDIAN - def __init__ (self): - self.fuzz_complete = False # this flag is raised when the mutations are exhausted. - self.fuzz_library = [] # library of static fuzz heuristics to cycle through. - self.fuzzable = True # flag controlling whether or not the given primitive is to be fuzzed. - self.mutant_index = 0 # current mutation index into the fuzz library. - self.original_value = None # original value of primitive. - self.rendered = "" # rendered value of primitive. - self.value = None # current value of primitive. +# TODO: Change primitives to yield instead of returning a bunch of trues (if possible) - def exhaust (self): - ''' +class BasePrimitive(object): + """ + The primitive base class implements common functionality shared across most primitives. + """ + + def __init__(self): + self.fuzz_complete = False # this flag is raised when the mutations are exhausted. + self.fuzz_library = [] # library of static fuzz heuristics to cycle through. + self.fuzzable = True # flag controlling whether or not the given primitive is to be fuzzed. + self.mutant_index = 0 # current mutation index into the fuzz library. + self.original_value = None # original value of primitive. + self.rendered = "" # rendered value of primitive. + self.value = None # current value of primitive. + + def exhaust(self): + """ Exhaust the possible mutations for this primitive. - @rtype: Integer + @rtype: int @return: The number of mutations to reach exhaustion - ''' + """ num = self.num_mutations() - self.mutant_index - self.fuzz_complete = True - self.mutant_index = self.num_mutations() - self.value = self.original_value + self.fuzz_complete = True + self.mutant_index = self.num_mutations() + self.value = self.original_value return num - - def mutate (self): - ''' + def mutate(self): + """ Mutate the primitive by stepping through the fuzz library, return False on completion. - @rtype: Boolean + @rtype: bool @return: True on success, False otherwise. - ''' + """ # if we've ran out of mutations, raise the completion flag. if self.mutant_index == self.num_mutations(): @@ -59,66 +61,57 @@ def mutate (self): return True - - def num_mutations (self): - ''' + def num_mutations(self): + """ Calculate and return the total number of mutations for this individual primitive. - @rtype: Integer + @rtype: int @return: Number of mutated forms this primitive can take - ''' + """ return len(self.fuzz_library) - - def render (self): - ''' + def render(self): + """ Nothing fancy on render, simply return the value. - ''' + """ self.rendered = self.value return self.rendered - - def reset (self): - ''' + def reset(self): + """ Reset this primitive to the starting mutation state. - ''' + """ + + self.fuzz_complete = False + self.mutant_index = 0 + self.value = self.original_value - self.fuzz_complete = False - self.mutant_index = 0 - self.value = self.original_value + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, repr(self.value)) -######################################################################################################################## -class delim (base_primitive): - def __init__ (self, value, fuzzable=True, name=None): - ''' +class Delim(BasePrimitive): + def __init__(self, value=None, fuzzable=True, name=None): + """ Represent a delimiter such as :,\r,\n, ,=,>,< etc... Mutations include repetition, substitution and exclusion. - @type value: Character + @type value: chr @param value: Original value - @type fuzzable: Boolean + @type fuzzable: bool @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type name: String + @type name: str @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' + """ - self.value = self.original_value = value - self.fuzzable = fuzzable - self.name = name + super(Delim, self).__init__() - self.s_type = "delim" # for ease of object identification - self.rendered = "" # rendered value - self.fuzz_complete = False # flag if this primitive has been completely fuzzed - self.fuzz_library = [] # library of fuzz heuristics - self.mutant_index = 0 # current mutation number + self.fuzzable = fuzzable + self.name = name + self.value = value + self.s_type = "delim" # for ease of object identification - # - # build the library of fuzz heuristics. - # - - # if the default delim is not blank, repeat it a bunch of times. if self.value: self.fuzz_library.append(self.value * 2) self.fuzz_library.append(self.value * 5) @@ -128,16 +121,12 @@ def __init__ (self, value, fuzzable=True, name=None): self.fuzz_library.append(self.value * 500) self.fuzz_library.append(self.value * 1000) - # try ommitting the delimiter. self.fuzz_library.append("") - - # if the delimiter is a space, try throwing out some tabs. if self.value == " ": self.fuzz_library.append("\t") self.fuzz_library.append("\t" * 2) self.fuzz_library.append("\t" * 100) - # toss in some other common delimiters: self.fuzz_library.append(" ") self.fuzz_library.append("\t") self.fuzz_library.append("\t " * 100) @@ -176,54 +165,51 @@ def __init__ (self, value, fuzzable=True, name=None): self.fuzz_library.append("\r\n" * 512) -######################################################################################################################## -class group (base_primitive): - def __init__ (self, name, values): - ''' +class Group(BasePrimitive): + def __init__(self, name, values): + """ This primitive represents a list of static values, stepping through each one on mutation. You can tie a block to a group primitive to specify that the block should cycle through all possible mutations for *each* value within the group. The group primitive is useful for example for representing a list of valid opcodes. - @type name: String + @type name: str @param name: Name of group - @type values: List or raw data + @type values: list or str @param values: List of possible raw values this group can take. - ''' + """ - self.name = name - self.values = values - self.fuzzable = True + super(Group, self).__init__() - self.s_type = "group" - self.value = self.values[0] - self.original_value = self.values[0] - self.rendered = "" - self.fuzz_complete = False - self.mutant_index = 0 - # sanity check that values list only contains strings (or raw data) - if self.values != []: - for val in self.values: - assert type(val) is str, "Value list may only contain strings or raw data" + self.name = name + self.values = values + self.s_type = "group" + assert len(self.values) > 0, "You can't have an empty value list for your group!" - def mutate (self): - ''' + self.value = self.original_value = self.values[0] + + for val in self.values: + assert isinstance(val, basestring), "Value list may only contain strings or raw data" + + def mutate(self): + """ Move to the next item in the values list. - @rtype: False + @rtype: bool @return: False - ''' - + """ + # TODO: See if num_mutations() can be done away with (me thinks yes). if self.mutant_index == self.num_mutations(): self.fuzz_complete = True # if fuzzing was disabled or complete, and mutate() is called, ensure the original value is restored. if not self.fuzzable or self.fuzz_complete: - self.value = self.values[0] + self.value = self.original_value return False # step through the value list. + # TODO: break this into a get_value() function, so we can keep mutate as close to standard as possible. self.value = self.values[self.mutant_index] # increment the mutation count. @@ -231,65 +217,61 @@ def mutate (self): return True - - def num_mutations (self): - ''' + def num_mutations(self): + """ Number of values in this primitive. - @rtype: Integer + @rtype: int @return: Number of values in this primitive. - ''' + """ return len(self.values) -######################################################################################################################## -class random_data (base_primitive): - def __init__ (self, value, min_length, max_length, max_mutations=25, fuzzable=True, step=None, name=None): - ''' - Generate a random chunk of data while maintaining a copy of the original. A random length range can be specified. +class RandomData(BasePrimitive): + def __init__(self, value, min_length, max_length, max_mutations=25, fuzzable=True, step=None, name=None): + """ + Generate a random chunk of data while maintaining a copy of the original. A random length range + can be specified. + For a static length, set min/max length to be the same. - @type value: Raw + @type value: str @param value: Original value - @type min_length: Integer + @type min_length: int @param min_length: Minimum length of random block - @type max_length: Integer + @type max_length: int @param max_length: Maximum length of random block - @type max_mutations: Integer + @type max_mutations: int @param max_mutations: (Optional, def=25) Number of mutations to make before reverting to default - @type fuzzable: Boolean + @type fuzzable: bool @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type step: Integer + @type step: int @param step: (Optional, def=None) If not null, step count between min and max reps, otherwise random - @type name: String + @type name: str @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' - - self.value = self.original_value = str(value) - self.min_length = min_length - self.max_length = max_length - self.max_mutations = max_mutations - self.fuzzable = fuzzable - self.step = step - self.name = name + """ - self.s_type = "random_data" # for ease of object identification - self.rendered = "" # rendered value - self.fuzz_complete = False # flag if this primitive has been completely fuzzed - self.mutant_index = 0 # current mutation number + super(RandomData, self).__init__() + self.value = self.original_value = str(value) + self.min_length = min_length + self.max_length = max_length + self.max_mutations = max_mutations + self.fuzzable = fuzzable + self.step = step + self.name = name + self.s_type = "random_data" # for ease of object identification if self.step: self.max_mutations = (self.max_length - self.min_length) / self.step + 1 - - def mutate (self): - ''' + def mutate(self): + """ Mutate the primitive value returning False on completion. - @rtype: Boolean + @rtype: bool @return: True on success, False otherwise. - ''' + """ # if we've ran out of mutations, raise the completion flag. if self.mutant_index == self.num_mutations(): @@ -317,182 +299,153 @@ def mutate (self): return True - - def num_mutations (self): - ''' + def num_mutations(self): + """ Calculate and return the total number of mutations for this individual primitive. - @rtype: Integer + @rtype: int @return: Number of mutated forms this primitive can take - ''' + """ return self.max_mutations -######################################################################################################################## -class static (base_primitive): - def __init__ (self, value, name=None): - ''' +class Static(BasePrimitive): + def __init__(self, value, name=None): + """ Primitive that contains static content. - @type value: Raw + @type value: str @param value: Raw static data - @type name: String + @type name: str @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' - - self.value = self.original_value = value - self.name = name - self.fuzzable = False # every primitive needs this attribute. - self.mutant_index = 0 - self.s_type = "static" # for ease of object identification - self.rendered = "" - self.fuzz_complete = True + """ + super(Static, self).__init__() - def mutate (self): - ''' - Do nothing. - - @rtype: False - @return: False - ''' - + self.fuzz_complete = True + self.fuzzable = False + self.value = self.original_value = value + self.name = name + self.s_type = "static" + + def mutate(self): + """ + Always return false, don't fuzz + """ return False - - def num_mutations (self): - ''' - Return 0. - - @rtype: 0 - @return: 0 - ''' - + def num_mutations(self): + """ + We have no mutations + """ return 0 -######################################################################################################################## -class string (base_primitive): +class String(BasePrimitive): # store fuzz_library as a class variable to avoid copying the ~70MB structure across each instantiated primitive. fuzz_library = [] - def __init__ (self, value, size=-1, padding="\x00", encoding="ascii", fuzzable=True, max_len=0, name=None): - ''' + def __init__(self, value, size=-1, padding="\x00", encoding="ascii", fuzzable=True, max_len=0, name=None): + """ Primitive that cycles through a library of "bad" strings. The class variable 'fuzz_library' contains a list of smart fuzz values global across all instances. The 'this_library' variable contains fuzz values specific to the instantiated primitive. This allows us to avoid copying the near ~70MB fuzz_library data structure across each instantiated primitive. - @type value: String + @type value: str @param value: Default string value - @type size: Integer + @type size: int @param size: (Optional, def=-1) Static size of this field, leave -1 for dynamic. - @type padding: Character + @type padding: chr @param padding: (Optional, def="\\x00") Value to use as padding to fill static field size. - @type encoding: String - @param encoding: (Optonal, def="ascii") String encoding, ex: utf_16_le for Microsoft Unicode. - @type fuzzable: Boolean + @type encoding: str + @param encoding: (Optional, def="ascii") String encoding, ex: utf_16_le for Microsoft Unicode. + @type fuzzable: bool @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type max_len: Integer + @type max_len: int @param max_len: (Optional, def=0) Maximum string length - @type name: String + @type name: str @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' - - self.value = self.original_value = value - self.size = size - self.padding = padding - self.encoding = encoding - self.fuzzable = fuzzable - self.name = name + """ - self.s_type = "string" # for ease of object identification - self.rendered = "" # rendered value - self.fuzz_complete = False # flag if this primitive has been completely fuzzed - self.mutant_index = 0 # current mutation number + super(String, self).__init__() - # add this specific primitives repitition values to the unique fuzz library. + self.value = self.original_value = value + self.size = size + self.padding = padding + self.encoding = encoding + self.fuzzable = fuzzable + self.name = name + self.s_type = "string" # for ease of object identification self.this_library = \ - [ - self.value * 2, - self.value * 10, - self.value * 100, - - # UTF-8 - self.value * 2 + "\xfe", - self.value * 10 + "\xfe", - self.value * 100 + "\xfe", - ] - - # if the fuzz library has not yet been initialized, do so with all the global values. - if not self.fuzz_library: - string.fuzz_library = \ [ - # omission. - "", - - # strings ripped from spike (and some others I added) - "/.:/" + "A"*5000 + "\x00\x00", - "/.../" + "A"*5000 + "\x00\x00", - "/.../.../.../.../.../.../.../.../.../.../", - "/../../../../../../../../../../../../etc/passwd", - "/../../../../../../../../../../../../boot.ini", - "..:..:..:..:..:..:..:..:..:..:..:..:..:", - "\\\\*", - "\\\\?\\", - "/\\" * 5000, - "/." * 5000, - "!@#$%%^#$%#$@#$%$$@#$%^^**(()", - "%01%02%03%04%0a%0d%0aADSF", - "%01%02%03@%04%0a%0d%0aADSF", - "/%00/", - "%00/", - "%00", - "%u0000", - "%\xfe\xf0%\x00\xff", - "%\xfe\xf0%\x01\xff" * 20, - - # format strings. - "%n" * 100, - "%n" * 500, - "\"%n\"" * 500, - "%s" * 100, - "%s" * 500, - "\"%s\"" * 500, - - # command injection. - "|touch /tmp/SULLEY", - ";touch /tmp/SULLEY;", - "|notepad", - ";notepad;", - "\nnotepad\n", - - # SQL injection. - "1;SELECT%20*", - "'sqlattempt1", - "(sqlattempt2)", - "OR%201=1", - - # some binary strings. - "\xde\xad\xbe\xef", - "\xde\xad\xbe\xef" * 10, - "\xde\xad\xbe\xef" * 100, - "\xde\xad\xbe\xef" * 1000, - "\xde\xad\xbe\xef" * 10000, - "\x00" * 1000, - - # miscellaneous. - "\r\n" * 100, - "<>" * 500, # sendmail crackaddr (http://lsd-pl.net/other/sendmail.txt) + self.value * 2, + self.value * 10, + self.value * 100, + + # UTF-8 + # TODO: This can't actually convert these to unicode strings... + self.value * 2 + "\xfe", + self.value * 10 + "\xfe", + self.value * 100 + "\xfe", ] + if not self.fuzz_library: + self.fuzz_library = \ + [ + "", + # strings ripped from spike (and some others I added) + "/.:/" + "A" * 5000 + "\x00\x00", + "/.../" + "B" * 5000 + "\x00\x00", + "/.../.../.../.../.../.../.../.../.../.../", + "/../../../../../../../../../../../../etc/passwd", + "/../../../../../../../../../../../../boot.ini", + "..:..:..:..:..:..:..:..:..:..:..:..:..:", + "\\\\*", + "\\\\?\\", + "/\\" * 5000, + "/." * 5000, + "!@#$%%^#$%#$@#$%$$@#$%^^**(()", + "%01%02%03%04%0a%0d%0aADSF", + "%01%02%03@%04%0a%0d%0aADSF", + "\x01\x02\x03\x04", + "/%00/", + "%00/", + "%00", + "%u0000", + "%\xfe\xf0%\x00\xff", + "%\xfe\xf0%\x01\xff" * 20, + + # format strings. + "%n" * 100, + "%n" * 500, + "\"%n\"" * 500, + "%s" * 100, + "%s" * 500, + "\"%s\"" * 500, + + # command injection. + "|touch /tmp/SULLEY", + ";touch /tmp/SULLEY;", + "|notepad", + ";notepad;", + "\nnotepad\n", + + # some binary strings. + "\xde\xad\xbe\xef", + "\xde\xad\xbe\xef" * 10, + "\xde\xad\xbe\xef" * 100, + "\xde\xad\xbe\xef" * 1000, + "\xde\xad\xbe\xef" * 10000, + + # miscellaneous. + "\r\n" * 100, + "<>" * 500, # sendmail crackaddr (http://lsd-pl.net/other/sendmail.txt) + ] # add some long strings. - self.add_long_strings("A") - self.add_long_strings("B") + self.add_long_strings("C") self.add_long_strings("1") - self.add_long_strings("2") - self.add_long_strings("3") self.add_long_strings("<") self.add_long_strings(">") self.add_long_strings("'") @@ -516,63 +469,62 @@ def __init__ (self, value, size=-1, padding="\x00", encoding="ascii", fuzzable=T self.add_long_strings("{") self.add_long_strings("}") self.add_long_strings("\x14") - self.add_long_strings("\xFE") # expands to 4 characters under utf16 - self.add_long_strings("\xFF") # expands to 4 characters under utf16 + self.add_long_strings("\x00") + self.add_long_strings("\xFE") # expands to 4 characters under utf16 + self.add_long_strings("\xFF") # expands to 4 characters under utf16 - # add some long strings with null bytes thrown in the middle of it. + # add some long strings with null bytes thrown in the middle of them. for length in [128, 256, 1024, 2048, 4096, 32767, 0xFFFF]: - s = "B" * length - s = s[:len(s)/2] + "\x00" + s[len(s)/2:] - string.fuzz_library.append(s) - - # if the optional file '.fuzz_strings' is found, parse each line as a new entry for the fuzz library. - try: - fh = open(".fuzz_strings", "r") + s = "D" * length + # Number of null bytes to insert (random) + for i in range(random.randint(1, 10)): + # Location of random byte + loc = random.randint(1, len(s)) + s = s[:loc] + "\x00" + s[loc:] + self.fuzz_library.append(s) - for fuzz_string in fh.readlines(): - fuzz_string = fuzz_string.rstrip("\r\n") + # TODO: Add easy and sane string injection from external file/s - if fuzz_string != "": - string.fuzz_library.append(fuzz_string) - - fh.close() - except: - pass - - # delete strings which length is greater than max_len. + # TODO: Make this more clear if max_len > 0: + # If any of our strings are over max_len if any(len(s) > max_len for s in self.this_library): + # Pull out only the ones that aren't self.this_library = list(set([s[:max_len] for s in self.this_library])) - + # Same thing here if any(len(s) > max_len for s in self.fuzz_library): self.fuzz_library = list(set([s[:max_len] for s in self.fuzz_library])) - - def add_long_strings (self, sequence): - ''' + def add_long_strings(self, sequence): + """ Given a sequence, generate a number of selectively chosen strings lengths of the given sequence and add to the string heuristic library. - @type sequence: String + @type sequence: str @param sequence: Sequence to repeat for creation of fuzz strings. - ''' - - for length in [128, 255, 256, 257, 511, 512, 513, 1023, 1024, 2048, 2049, 4095, 4096, 4097, 5000, 10000, 20000, - 32762, 32763, 32764, 32765, 32766, 32767, 32768, 32769, 0xFFFF-2, 0xFFFF-1, 0xFFFF, 0xFFFF+1, - 0xFFFF+2, 99999, 100000, 500000, 1000000]: - - long_string = sequence * length - string.fuzz_library.append(long_string) - - - def mutate (self): - ''' + """ + strings = [] + for size in [128, 256, 512, 1024, 2048, 4096, 32768, 0xFFFF]: + strings.append(sequence * (size - 2)) + strings.append(sequence * (size - 1)) + strings.append(sequence * size) + strings.append(sequence * (size + 1)) + strings.append(sequence * (size + 2)) + + for size in [5000, 10000, 20000, 99999, 100000, 500000, 1000000]: + strings.append(sequence * size) + + for string in strings: + self.fuzz_library.append(string) + + def mutate(self): + """ Mutate the primitive by stepping through the fuzz library extended with the "this" library, return False on completion. - @rtype: Boolean + @rtype: bool @return: True on success, False otherwise. - ''' + """ # loop through the fuzz library until a suitable match is found. while 1: @@ -595,36 +547,33 @@ def mutate (self): if self.size == -1: break - # ignore library items greather then user-supplied length. + # ignore library items greater then user-supplied length. # TODO: might want to make this smarter. if len(self.value) > self.size: continue # pad undersized library items. if len(self.value) < self.size: - self.value = self.value + self.padding * (self.size - len(self.value)) + self.value += self.padding * (self.size - len(self.value)) break return True - - def num_mutations (self): - ''' + def num_mutations(self): + """ Calculate and return the total number of mutations for this individual primitive. - @rtype: Integer + @rtype: int @return: Number of mutated forms this primitive can take - ''' - + """ return len(self.fuzz_library) + len(self.this_library) - - def render (self): - ''' + def render(self): + """ Render the primitive, encode the string according to the specified encoding. - ''' - + """ # try to encode the string properly and fall back to the default value on failure. + # TODO: Fix this - seems hacky try: self.rendered = str(self.value).encode(self.encoding) except: @@ -633,55 +582,52 @@ def render (self): return self.rendered -######################################################################################################################## -class bit_field (base_primitive): - def __init__ (self, value, width, max_num=None, endian="<", format="binary", signed=False, full_range=False, fuzzable=True, name=None): - ''' +class BitField(BasePrimitive): + def __init__(self, value, width, max_num=None, endian=LITTLE_ENDIAN, output_format="binary", signed=False, + full_range=False, fuzzable=True, name=None): + """ The bit field primitive represents a number of variable length and is used to define all other integer types. - @type value: Integer - @param value: Default integer value - @type width: Integer - @param width: Width of bit fields - @type endian: Character - @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) - @type format: String - @param format: (Optional, def=binary) Output format, "binary" or "ascii" - @type signed: Boolean - @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") - @type full_range: Boolean - @param full_range: (Optional, def=False) If enabled the field mutates through *all* possible values. - @type fuzzable: Boolean - @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive - @type name: String - @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive - ''' - - assert(type(value) is int or type(value) is long) - assert(type(width) is int or type(value) is long) - - - self.value = self.original_value = value - self.width = width - self.max_num = max_num - self.endian = endian - self.format = format - self.signed = signed - self.full_range = full_range - self.fuzzable = fuzzable - self.name = name - - self.rendered = "" # rendered value - self.fuzz_complete = False # flag if this primitive has been completely fuzzed - self.fuzz_library = [] # library of fuzz heuristics - self.mutant_index = 0 # current mutation number - - if self.max_num == None: + @type value: int + @param value: Default integer value + @type width: int + @param width: Width of bit fields + @type max_num: int + @param max_num: Maximum number to iterate up to + @type endian: chr + @param endian: (Optional, def=LITTLE_ENDIAN) Endianess of the bit field (LITTLE_ENDIAN: <, BIG_ENDIAN: >) + @type output_format: str + @param output_format: (Optional, def=binary) Output format, "binary" or "ascii" + @type signed: bool + @param signed: (Optional, def=False) Make size signed vs. unsigned (applicable only with format="ascii") + @type full_range: bool + @param full_range: (Optional, def=False) If enabled the field mutates through *all* possible values. + @type fuzzable: bool + @param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive + @type name: str + @param name: (Optional, def=None) Specifying a name gives you direct access to a primitive + """ + + super(BitField, self).__init__() + + assert isinstance(value, (int, long)), "value must be an integer!" + assert isinstance(width, (int, long)), "width must be an integer!" + + self.value = self.original_value = value + self.width = width + self.max_num = max_num + self.endian = endian + self.format = output_format + self.signed = signed + self.full_range = full_range + self.fuzzable = fuzzable + self.name = name + + if not self.max_num: self.max_num = self.to_decimal("1" * width) - assert(type(self.max_num) is int or type(self.max_num) is long) + assert isinstance(self.max_num, (int, long)), "max_num must be an integer!" - # build the fuzz library. if self.full_range: # add all possible values. for i in xrange(0, self.max_num): @@ -697,80 +643,54 @@ def __init__ (self, value, width, max_num=None, endian="<", format="binary", sig self.add_integer_boundaries(self.max_num / 32) self.add_integer_boundaries(self.max_num) - # if the optional file '.fuzz_ints' is found, parse each line as a new entry for the fuzz library. - try: - fh = open(".fuzz_ints", "r") - - for fuzz_int in fh.readlines(): - # convert the line into an integer, continue on failure. - try: - fuzz_int = long(fuzz_int, 16) - except: - continue - - if fuzz_int <= self.max_num: - self.fuzz_library.append(fuzz_int) - - fh.close() - except: - pass - + # TODO: Add injectable arbitrary bit fields - def add_integer_boundaries (self, integer): - ''' + def add_integer_boundaries(self, integer): + """ Add the supplied integer and border cases to the integer fuzz heuristics library. - @type integer: Int - @param integer: Integer to append to fuzz heuristics - ''' - + @type integer: int + @param integer: int to append to fuzz heuristics + """ for i in xrange(-10, 10): case = integer + i - # ensure the border case falls within the valid range for this field. if 0 <= case <= self.max_num: if case not in self.fuzz_library: self.fuzz_library.append(case) - - def render (self): - ''' + def render(self): + """ Render the primitive. - ''' - - # - # binary formatting. - # + """ if self.format == "binary": bit_stream = "" - rendered = "" + rendered = "" # pad the bit stream to the next byte boundary. if self.width % 8 == 0: bit_stream += self.to_binary() else: - bit_stream = "0" * (8 - (self.width % 8)) + bit_stream = "0" * (8 - (self.width % 8)) bit_stream += self.to_binary() # convert the bit stream from a string of bits into raw bytes. for i in xrange(len(bit_stream) / 8): - chunk = bit_stream[8*i:8*i+8] + chunk_min = 8 * i + chunk_max = chunk_min + 8 + chunk = bit_stream[chunk_min:chunk_max] rendered += struct.pack("B", self.to_decimal(chunk)) # if necessary, convert the endianess of the raw bytes. - if self.endian == "<": + if self.endian == LITTLE_ENDIAN: rendered = list(rendered) rendered.reverse() rendered = "".join(rendered) self.rendered = rendered - - # - # ascii formatting. - # - else: + # Otherwise we have ascii/something else # if the sign flag is raised and we are dealing with a signed integer (first bit is 1). if self.signed and self.to_binary()[0] == "1": max_num = self.to_decimal("0" + "1" * (self.width - 1)) @@ -781,7 +701,7 @@ def render (self): val = max_num - val # toss in the negative sign. - self.rendered = "%d" % ~val + self.rendered = "%d" % (-val - 1) # unsigned integer or positive signed integer. else: @@ -789,78 +709,92 @@ def render (self): return self.rendered - - def to_binary (self, number=None, bit_count=None): - ''' + def to_binary(self, number=None, bit_count=None): + """ Convert a number to a binary string. - @type number: Integer + @type number: int @param number: (Optional, def=self.value) Number to convert - @type bit_count: Integer + @type bit_count: int @param bit_count: (Optional, def=self.width) Width of bit string - @rtype: String + @rtype: str @return: Bit string - ''' + """ - if number == None: + if not number: number = self.value - if bit_count == None: + if not bit_count: bit_count = self.width - return "".join(map(lambda x:str((number >> x) & 1), range(bit_count -1, -1, -1))) + return "".join(map(lambda x: str((number >> x) & 1), range(bit_count - 1, -1, -1))) - - def to_decimal (self, binary): - ''' + # noinspection PyMethodMayBeStatic + def to_decimal(self, binary): + """ Convert a binary string to a decimal number. - @type binary: String + @type binary: str @param binary: Binary string - @rtype: Integer + @rtype: int @return: Converted bit string - ''' + """ return int(binary, 2) -######################################################################################################################## -class byte (bit_field): - def __init__ (self, value, endian="<", format="binary", signed=False, full_range=False, fuzzable=True, name=None): - self.s_type = "byte" - if type(value) not in [int, long]: - value = struct.unpack(endian + "B", value)[0] +class Byte(BitField): + def __init__(self, value, *args, **kwargs): + # Inject the one parameter we care to pass in (width) + width = 8 + max_num = None + + super(Byte, self).__init__(value, width, max_num, *args, **kwargs) + + self.s_type = "byte" + + if type(self.value) not in [int, long]: + self.value = struct.unpack(self.endian + "B", self.value)[0] + + +class Word(BitField): + def __init__(self, value, *args, **kwargs): + # Inject our width argument + width = 16 + max_num = None + + super(Word, self).__init__(value, width, max_num, *args, **kwargs) + + self.s_type = "word" + + if type(self.value) not in [int, long]: + self.value = struct.unpack(self.endian + "H", self.value)[0] - bit_field.__init__(self, value, 8, None, endian, format, signed, full_range, fuzzable, name) +class DWord(BitField): + def __init__(self, value, *args, **kwargs): + # Inject our width argument + width = 32 + max_num = None -######################################################################################################################## -class word (bit_field): - def __init__ (self, value, endian="<", format="binary", signed=False, full_range=False, fuzzable=True, name=None): - self.s_type = "word" - if type(value) not in [int, long]: - value = struct.unpack(endian + "H", value)[0] + super(DWord, self).__init__(value, width, max_num, *args, **kwargs) - bit_field.__init__(self, value, 16, None, endian, format, signed, full_range, fuzzable, name) + self.s_type = "dword" + if type(self.value) not in [int, long]: + self.value = struct.unpack(self.endian + "L", self.value)[0] -######################################################################################################################## -class dword (bit_field): - def __init__ (self, value, endian="<", format="binary", signed=False, full_range=False, fuzzable=True, name=None): - self.s_type = "dword" - if type(value) not in [int, long]: - value = struct.unpack(endian + "L", value)[0] - bit_field.__init__(self, value, 32, None, endian, format, signed, full_range, fuzzable, name) +class QWord(BitField): + def __init__(self, value, *args, **kwargs): + width = 64 + max_num = None + super(QWord, self).__init__(value, width, max_num, *args, **kwargs) -######################################################################################################################## -class qword (bit_field): - def __init__ (self, value, endian="<", format="binary", signed=False, full_range=False, fuzzable=True, name=None): - self.s_type = "qword" - if type(value) not in [int, long]: - value = struct.unpack(endian + "Q", value)[0] + self.s_type = "qword" - bit_field.__init__(self, value, 64, None, endian, format, signed, full_range, fuzzable, name) + if type(self.value) not in [int, long]: + self.value = struct.unpack(self.endian + "Q", self.value)[0] \ No newline at end of file diff --git a/sulley/sessions.py b/sulley/sessions.py index 64d8c4c..9ba31c8 100644 --- a/sulley/sessions.py +++ b/sulley/sessions.py @@ -1,54 +1,54 @@ -import os -import re import sys import zlib import time import socket -import httplib +import ssl +import signal import cPickle import threading -import BaseHTTPServer import httplib import logging - import blocks -import pedrpc import pgraph import sex import primitives +from helpers import get_max_udp_size + +from tornado.wsgi import WSGIContainer +from tornado.httpserver import HTTPServer +from tornado.ioloop import IOLoop +from web.app import app + -######################################################################################################################## -class target: - ''' +class Target(object): + """ Target descriptor container. - ''' + """ - def __init__ (self, host, port, **kwargs): - ''' - @type host: String + def __init__(self, host, port): + """ + @type host: str @param host: Hostname or IP address of target system - @type port: Integer + @type port: int @param port: Port of target service - ''' + """ - self.host = host - self.port = port + self.host = host + self.port = port # set these manually once target is instantiated. - self.netmon = None - self.procmon = None - self.vmcontrol = None - self.netmon_options = {} - self.procmon_options = {} + self.netmon = None + self.procmon = None + self.vmcontrol = None + self.netmon_options = {} + self.procmon_options = {} self.vmcontrol_options = {} - - def pedrpc_connect (self): - ''' + def pedrpc_connect(self): + """ Pass specified target parameters to the PED-RPC server. - ''' - + """ # If the process monitor is alive, set it's options if self.procmon: while 1: @@ -80,10 +80,9 @@ def pedrpc_connect (self): eval('self.netmon.set_%s(self.netmon_options["%s"])' % (key, key)) -######################################################################################################################## -class connection (pgraph.edge.edge): - def __init__ (self, src, dst, callback=None): - ''' +class Connection(pgraph.Edge): + def __init__(self, src, dst, callback=None): + """ Extends pgraph.edge with a callback option. This allows us to register a function to call between node transmissions to implement functionality such as challenge response systems. The callback method must follow this prototype:: @@ -95,90 +94,83 @@ def callback(session, node, edge, sock) the data returned from the last socket transmission and sock is the live socket. A callback is also useful in situations where, for example, the size of the next packet is specified in the first packet. - @type src: Integer + @type src: int @param src: Edge source ID - @type dst: Integer + @type dst: int @param dst: Edge destination ID - @type callback: Function + @type callback: def @param callback: (Optional, def=None) Callback function to pass received data to between node xmits - ''' + """ - # run the parent classes initialization routine first. - pgraph.edge.edge.__init__(self, src, dst) + super(Connection, self).__init__(src, dst) self.callback = callback -######################################################################################################################## -class session (pgraph.graph): - def __init__( - self, - session_filename=None, - skip=0, - sleep_time=1.0, - log_level=logging.INFO, - logfile=None, - logfile_level=logging.DEBUG, - proto="tcp", - bind=None, - restart_interval=0, - timeout=5.0, - web_port=26000, - crash_threshold=3, - restart_sleep_time=300 - ): - ''' +class Session(pgraph.Graph): + def __init__(self, session_filename=None, skip=0, sleep_time=1.0, log_level=logging.INFO, logfile=None, + logfile_level=logging.DEBUG, proto="tcp", bind=None, restart_interval=0, timeout=5.0, web_port=26000, + crash_threshold=3, restart_sleep_time=300): + """ Extends pgraph.graph and provides a container for architecting protocol dialogs. - @type session_filename: String - @kwarg session_filename: (Optional, def=None) Filename to serialize persistant data to - @type skip: Integer + @type session_filename: str + @kwarg session_filename: (Optional, def=None) Filename to serialize persistent data to + @type skip: int @kwarg skip: (Optional, def=0) Number of test cases to skip - @type sleep_time: Float + @type sleep_time: float @kwarg sleep_time: (Optional, def=1.0) Time to sleep in between tests - @type log_level: Integer + @type log_level: int @kwarg log_level: (Optional, def=logger.INFO) Set the log level - @type logfile: String + @type logfile: str @kwarg logfile: (Optional, def=None) Name of log file - @type logfile_level: Integer + @type logfile_level: int @kwarg logfile_level: (Optional, def=logger.INFO) Set the log level for the logfile - @type proto: String + @type proto: str @kwarg proto: (Optional, def="tcp") Communication protocol ("tcp", "udp", "ssl") - @type bind: Tuple (host, port) + @type bind: tuple (host, port) @kwarg bind: (Optional, def=random) Socket bind address and port - @type timeout: Float + @type timeout: float @kwarg timeout: (Optional, def=5.0) Seconds to wait for a send/recv prior to timing out - @type restart_interval: Integer + @type restart_interval: int @kwarg restart_interval (Optional, def=0) Restart the target after n test cases, disable by setting to 0 - @type crash_threshold: Integer + @type crash_threshold: int @kwarg crash_threshold (Optional, def=3) Maximum number of crashes allowed before a node is exhaust - @type restart_sleep_time: Integer + @type restart_sleep_time: int @kwarg restart_sleep_time: Optional, def=300) Time in seconds to sleep when target can't be restarted - @type web_port: Integer - @kwarg web_port: (Optional, def=26000) Port for monitoring fuzzing campaign via a web browser - ''' - - # run the parent classes initialization routine first. - pgraph.graph.__init__(self) - - self.session_filename = session_filename - self.skip = skip - self.sleep_time = sleep_time - self.proto = proto.lower() - self.bind = bind - self.ssl = False - self.restart_interval = restart_interval - self.timeout = timeout - self.web_port = web_port - self.crash_threshold = crash_threshold - self.restart_sleep_time = restart_sleep_time + @type web_port: int + @kwarg web_port: (Optional, def=26000) Port for monitoring fuzzing campaign via a web browser + """ + + super(Session, self).__init__() + + self.max_udp = get_max_udp_size() + try: + import signal + + self.signal_module = True + except: + self.signal_module = False + + self.web_interface_thread = self.build_webapp_thread(port=26000) + self.session_filename = session_filename + self.skip = skip + self.sleep_time = sleep_time + self.proto = proto.lower() + self.bind = bind + self.ssl = False + self.restart_interval = restart_interval + self.timeout = timeout + self.web_port = web_port + self.crash_threshold = crash_threshold + self.restart_sleep_time = restart_sleep_time # Initialize logger self.logger = logging.getLogger("Sulley_logger") self.logger.setLevel(log_level) formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] -> %(message)s') - if logfile != None: + if logfile: filehandler = logging.FileHandler(logfile) filehandler.setLevel(logfile_level) filehandler.setFormatter(formatter) @@ -187,16 +179,17 @@ def __init__( consolehandler = logging.StreamHandler() consolehandler.setFormatter(formatter) consolehandler.setLevel(log_level) + self.logger.addHandler(consolehandler) self.total_num_mutations = 0 - self.total_mutant_index = 0 - self.fuzz_node = None - self.targets = [] - self.netmon_results = {} - self.procmon_results = {} - self.protmon_results = {} - self.pause_flag = False + self.total_mutant_index = 0 + self.fuzz_node = None + self.targets = [] + self.netmon_results = {} + self.procmon_results = {} + self.protmon_results = {} + self.is_paused = False self.crashing_primitives = {} if self.proto == "tcp": @@ -204,7 +197,7 @@ def __init__( elif self.proto == "ssl": self.proto = socket.SOCK_STREAM - self.ssl = True + self.ssl = True elif self.proto == "udp": self.proto = socket.SOCK_DGRAM @@ -217,41 +210,37 @@ def __init__( # create a root node. we do this because we need to start fuzzing from a single point and the user may want # to specify a number of initial requests. - self.root = pgraph.node() - self.root.name = "__ROOT_NODE__" + self.root = pgraph.Node() + self.root.name = "__ROOT_NODE__" self.root.label = self.root.name - self.last_recv = None + self.last_recv = None self.add_node(self.root) - - #################################################################################################################### - def add_node (self, node): - ''' + def add_node(self, node): + """ Add a pgraph node to the graph. We overload this routine to automatically generate and assign an ID whenever a node is added. @type node: pGRAPH Node @param node: Node to add to session graph - ''' + """ node.number = len(self.nodes) - node.id = len(self.nodes) + node.id = len(self.nodes) - if not self.nodes.has_key(node.id): + if not node.id in self.nodes: self.nodes[node.id] = node return self - - #################################################################################################################### - def add_target (self, target): - ''' + def add_target(self, target): + """ Add a target to the session. Multiple targets can be added for parallel fuzzing. @type target: session.target @param target: Target to add to session - ''' + """ # pass specified target parameters to the PED-RPC server. target.pedrpc_connect() @@ -259,10 +248,8 @@ def add_target (self, target): # add target to internal list. self.targets.append(target) - - #################################################################################################################### - def connect (self, src, dst=None, callback=None): - ''' + def connect(self, src, dst=None, callback=None): + """ Create a connection between the two requests (nodes) and register an optional callback to process in between transmissions of the source and destination request. Leverage this functionality to handle situations such as challenge response systems. The session class maintains a top level node that all initial requests must be @@ -281,22 +268,22 @@ def connect (self, src, dst=None, callback=None): def callback(session, node, edge, sock) Where node is the node about to be sent, edge is the last edge along the current fuzz path to "node", session - is a pointer to the session instance which is useful for snagging data such as sesson.last_recv which contains + is a pointer to the session instance which is useful for snagging data such as session.last_recv which contains the data returned from the last socket transmission and sock is the live socket. A callback is also useful in situations where, for example, the size of the next packet is specified in the first packet. As another example, if you need to fill in the dynamic IP address of the target register a callback that snags the IP from sock.getpeername()[0]. - @type src: String or Request (Node) + @type src: str or Request (Node) @param src: Source request name or request node - @type dst: String or Request (Node) + @type dst: str or Request (Node) @param dst: Destination request name or request node - @type callback: Function + @type callback: def @param callback: (Optional, def=None) Callback function to pass received data to between node xmits - @rtype: pgraph.edge + @rtype: pgraph.Edge @return: The edge between the src and dst. - ''' + """ # if only a source was provided, then make it the destination and set the source to the root node. if not dst: @@ -318,70 +305,72 @@ def callback(session, node, edge, sock) self.add_node(dst) # create an edge between the two nodes and add it to the graph. - edge = connection(src.id, dst.id, callback) + edge = Connection(src.id, dst.id, callback) self.add_edge(edge) return edge - - #################################################################################################################### - def export_file (self): - ''' + def export_file(self): + """ Dump various object values to disk. @see: import_file() - ''' + """ if not self.session_filename: return - data = {} - data["session_filename"] = self.session_filename - data["skip"] = self.total_mutant_index - data["sleep_time"] = self.sleep_time - data["restart_sleep_time"] = self.restart_sleep_time - data["proto"] = self.proto - data["restart_interval"] = self.restart_interval - data["timeout"] = self.timeout - data["web_port"] = self.web_port - data["crash_threshold"] = self.crash_threshold - data["total_num_mutations"] = self.total_num_mutations - data["total_mutant_index"] = self.total_mutant_index - data["netmon_results"] = self.netmon_results - data["procmon_results"] = self.procmon_results - data['protmon_results'] = self.protmon_results - data["pause_flag"] = self.pause_flag + data = { + "session_filename": self.session_filename, + "skip": self.total_mutant_index, + "sleep_time": self.sleep_time, + "restart_sleep_time": self.restart_sleep_time, + "proto": self.proto, + "restart_interval": self.restart_interval, + "timeout": self.timeout, + "web_port": self.web_port, + "crash_threshold": self.crash_threshold, + "total_num_mutations": self.total_num_mutations, + "total_mutant_index": self.total_mutant_index, + "netmon_results": self.netmon_results, + "procmon_results": self.procmon_results, + "protmon_results": self.protmon_results, + "is_paused": self.is_paused + } fh = open(self.session_filename, "wb+") fh.write(zlib.compress(cPickle.dumps(data, protocol=2))) fh.close() - - #################################################################################################################### - def fuzz (self, this_node=None, path=[]): - ''' + def fuzz(self, this_node=None, path=()): + """ Call this routine to get the ball rolling. No arguments are necessary as they are both utilized internally during the recursive traversal of the session graph. - @type this_node: request (node) + @type this_node: node.Node @param this_node: (Optional, def=None) Current node that is being fuzzed. - @type path: List + @type path: list @param path: (Optional, def=[]) Nodes along the path to the current one being fuzzed. - ''' + """ # if no node is specified, then we start from the root node and initialize the session. if not this_node: # we can't fuzz if we don't have at least one target and one request. if not self.targets: - raise sex.SullyRuntimeError("NO TARGETS SPECIFIED IN SESSION") + raise sex.SullyRuntimeError("No targets specified in session") if not self.edges_from(self.root.id): - raise sex.SullyRuntimeError("NO REQUESTS SPECIFIED IN SESSION") + raise sex.SullyRuntimeError("No requests specified in session") this_node = self.root - try: self.server_init() - except: return + try: + self.server_init() + except: + return + + if isinstance(path, tuple): + path = list(path) # TODO: complete parallel fuzzing, will likely have to thread out each target target = self.targets[0] @@ -390,21 +379,20 @@ def fuzz (self, this_node=None, path=[]): for edge in self.edges_from(this_node.id): # the destination node is the one actually being fuzzed. self.fuzz_node = self.nodes[edge.dst] - num_mutations = self.fuzz_node.num_mutations() + num_mutations = self.fuzz_node.num_mutations() # keep track of the path as we fuzz through it, don't count the root node. # we keep track of edges as opposed to nodes because if there is more then one path through a set of # given nodes we don't want any ambiguity. path.append(edge) - current_path = " -> ".join([self.nodes[e.src].name for e in path[1:]]) + current_path = " -> ".join([self.nodes[e.src].name for e in path[1:]]) current_path += " -> %s" % self.fuzz_node.name self.logger.info("current fuzz path: %s" % current_path) self.logger.info("fuzzed %d of %d total cases" % (self.total_mutant_index, self.total_num_mutations)) done_with_fuzz_node = False - crash_count = 0 # loop through all possible mutations of the fuzz node. while not done_with_fuzz_node: @@ -427,15 +415,15 @@ def fuzz (self, this_node=None, path=[]): self.restart_target(target) # exception error handling routine, print log message and restart target. - def error_handler (e, msg, target, sock=None): - if sock: - sock.close() + def error_handler(error, msg, error_target, error_sock=None): + if error_sock: + error_sock.close() - msg += "\nException caught: %s" % repr(e) + msg += "\nException caught: %s" % repr(error) msg += "\nRestarting target and trying again" self.logger.critical(msg) - self.restart_target(target) + self.restart_target(error_target) # if we don't need to skip the current test case. if self.total_mutant_index > self.skip: @@ -485,8 +473,8 @@ def error_handler (e, msg, target, sock=None): # if SSL is requested, then enable it. if self.ssl: try: - ssl = socket.ssl(sock) - sock = httplib.FakeSocket(sock, ssl) + ssl_sock = ssl.wrap_socket(sock) + sock = httplib.FakeSocket(sock, ssl_sock) except Exception, e: error_handler(e, "failed ssl setup", target, sock) continue @@ -502,14 +490,14 @@ def error_handler (e, msg, target, sock=None): try: for e in path[:-1]: node = self.nodes[e.dst] - self.transmit(sock, node, e, target) + self.transmit(sock, node, e) except Exception, e: error_handler(e, "failed transmitting a node up the path", target, sock) continue # now send the current node we are fuzzing. try: - self.transmit(sock, self.fuzz_node, edge, target) + self.transmit(sock, self.fuzz_node, edge) except Exception, e: error_handler(e, "failed transmitting fuzz node", target, sock) continue @@ -518,7 +506,7 @@ def error_handler (e, msg, target, sock=None): break # if the user registered a post-send function, pass it the sock and let it do the deed. - # we do this outside the try/except loop because if our fuzz causes a crash then the post_send() + # We do this outside the try/except loop because if our fuzz causes a crash then the post_send() # will likely fail and we don't want to sit in an endless loop. try: self.post_send(sock) @@ -550,79 +538,68 @@ def error_handler (e, msg, target, sock=None): # wait for a signal only if fuzzing is finished (this function is recursive) # if fuzzing is not finished, web interface thread will catch it if self.total_mutant_index == self.total_num_mutations: - import signal while True: signal.pause() + else: + raise Exception("No signal.pause() on windows. #Fixme!") - - #################################################################################################################### - def import_file (self): - ''' - Load varous object values from disk. + def import_file(self): + """ + Load various object values from disk. @see: export_file() - ''' + """ try: - fh = open(self.session_filename, "rb") - data = cPickle.loads(zlib.decompress(fh.read())) - fh.close() + with open(self.session_filename, "rb") as f: + data = cPickle.loads(zlib.decompress(f.read())) except: return # update the skip variable to pick up fuzzing from last test case. - self.skip = data["total_mutant_index"] - - self.session_filename = data["session_filename"] - self.sleep_time = data["sleep_time"] - self.restart_sleep_time = data["restart_sleep_time"] - self.proto = data["proto"] - self.restart_interval = data["restart_interval"] - self.timeout = data["timeout"] - self.web_port = data["web_port"] - self.crash_threshold = data["crash_threshold"] + self.skip = data["total_mutant_index"] + self.session_filename = data["session_filename"] + self.sleep_time = data["sleep_time"] + self.restart_sleep_time = data["restart_sleep_time"] + self.proto = data["proto"] + self.restart_interval = data["restart_interval"] + self.timeout = data["timeout"] + self.web_port = data["web_port"] + self.crash_threshold = data["crash_threshold"] self.total_num_mutations = data["total_num_mutations"] - self.total_mutant_index = data["total_mutant_index"] - self.netmon_results = data["netmon_results"] - self.procmon_results = data["procmon_results"] - self.protmon_results = data["protmon_results"] - self.pause_flag = data["pause_flag"] - - - #################################################################################################################### - #def log (self, msg, level=1): - ''' - If the supplied message falls under the current log level, print the specified message to screen. - - @type msg: String - @param msg: Message to log - ''' -# - #if self.log_level >= level: - #print "[%s] %s" % (time.strftime("%I:%M.%S"), msg) - - - #################################################################################################################### - def num_mutations (self, this_node=None, path=[]): - ''' + self.total_mutant_index = data["total_mutant_index"] + self.netmon_results = data["netmon_results"] + self.procmon_results = data["procmon_results"] + self.protmon_results = data["protmon_results"] + self.is_paused = data["is_paused"] + + # noinspection PyMethodMayBeStatic + def log(self, msg, level=1): + raise Exception("Depreciated!") + + def num_mutations(self, this_node=None, path=()): + """ Number of total mutations in the graph. The logic of this routine is identical to that of fuzz(). See fuzz() - for inline comments. The member varialbe self.total_num_mutations is updated appropriately by this routine. + for inline comments. The member variable self.total_num_mutations is updated appropriately by this routine. @type this_node: request (node) @param this_node: (Optional, def=None) Current node that is being fuzzed. - @type path: List + @type path: list @param path: (Optional, def=[]) Nodes along the path to the current one being fuzzed. - @rtype: Integer + @rtype: int @return: Total number of mutations in this session. - ''' + """ if not this_node: - this_node = self.root + this_node = self.root self.total_num_mutations = 0 + if isinstance(path, tuple): + path = list(path) + for edge in self.edges_from(this_node.id): - next_node = self.nodes[edge.dst] + next_node = self.nodes[edge.dst] self.total_num_mutations += next_node.num_mutations() if edge.src != self.root.id: @@ -636,34 +613,28 @@ def num_mutations (self, this_node=None, path=[]): return self.total_num_mutations - - #################################################################################################################### - def pause (self): - ''' - If thet pause flag is raised, enter an endless loop until it is lowered. - ''' - + def pause(self): + """ + If that pause flag is raised, enter an endless loop until it is lowered. + """ while 1: - if self.pause_flag: + if self.is_paused: time.sleep(1) else: break - - #################################################################################################################### - def poll_pedrpc (self, target): - ''' + def poll_pedrpc(self, target): + """ Poll the PED-RPC endpoints (netmon, procmon etc...) for the target. @type target: session.target @param target: Session target whose PED-RPC services we are polling - ''' - + """ # kill the pcap thread and see how many bytes the sniffer recorded. if target.netmon: - bytes = target.netmon.post_send() - self.logger.info("netmon captured %d bytes for test case #%d" % (bytes, self.total_mutant_index)) - self.netmon_results[self.total_mutant_index] = bytes + captured_bytes = target.netmon.post_send() + self.logger.info("netmon captured %d bytes for test case #%d" % (captured_bytes, self.total_mutant_index)) + self.netmon_results[self.total_mutant_index] = captured_bytes # check if our fuzz crashed the target. procmon.post_send() returns False if the target access violated. if target.procmon and not target.procmon.post_send(): @@ -688,29 +659,25 @@ def poll_pedrpc (self, target): # if the user-supplied crash threshold is reached, exhaust this node. if self.crashing_primitives[self.fuzz_node.mutant] >= self.crash_threshold: # as long as we're not a group and not a repeat. - if not isinstance(self.fuzz_node.mutant, primitives.group): - if not isinstance(self.fuzz_node.mutant, blocks.repeat): + if not isinstance(self.fuzz_node.mutant, primitives.Group): + if not isinstance(self.fuzz_node.mutant, blocks.Repeat): skipped = self.fuzz_node.mutant.exhaust() - self.logger.warning("crash threshold reached for this primitive, exhausting %d mutants." % skipped) + self.logger.warning( + "Crash threshold reached for this primitive, exhausting %d mutants." % skipped + ) self.total_mutant_index += skipped self.fuzz_node.mutant_index += skipped # start the target back up. # If it returns False, stop the test - if self.restart_target(target, stop_first=False) == False: + if not self.restart_target(target, stop_first=False): self.logger.critical("Restarting the target failed, exiting.") self.export_file() - try: - self.thread.join() - except: - self.logger.debug("No server launched") sys.exit(0) - - - #################################################################################################################### - def post_send (self, sock): - ''' + # noinspection PyMethodMayBeStatic + def post_send(self, sock): + """ Overload or replace this routine to specify actions to run after to each fuzz request. The order of events is as follows:: @@ -720,17 +687,16 @@ def post_send (self, sock): @see: pre_send() - @type sock: Socket + @type sock: socket.socket @param sock: Connected socket to target - ''' + """ # default to doing nothing. pass - - #################################################################################################################### - def pre_send (self, sock): - ''' + # noinspection PyMethodMayBeStatic + def pre_send(self, sock): + """ Overload or replace this routine to specify actions to run prior to each fuzz request. The order of events is as follows:: @@ -742,21 +708,19 @@ def pre_send (self, sock): @type sock: Socket @param sock: Connected socket to target - ''' + """ # default to doing nothing. pass - - #################################################################################################################### - def restart_target (self, target, stop_first=True): - ''' + def restart_target(self, target, stop_first=True): + """ Restart the fuzz target. If a VMControl is available revert the snapshot, if a process monitor is available restart the target process. Otherwise, do nothing. @type target: session.target @param target: Target we are restarting - ''' + """ # vm restarting is the preferred method so try that first. if target.vmcontrol: @@ -777,7 +741,9 @@ def restart_target (self, target, stop_first=True): # otherwise all we can do is wait a while for the target to recover on its own. else: - self.logger.error("no vmcontrol or procmon channel available ... sleeping for %d seconds" % self.restart_sleep_time) + self.logger.error( + "no vmcontrol or procmon channel available ... sleeping for %d seconds" % self.restart_sleep_time + ) time.sleep(self.restart_sleep_time) # TODO: should be good to relaunch test for crash before returning False return False @@ -785,57 +751,44 @@ def restart_target (self, target, stop_first=True): # pass specified target parameters to the PED-RPC server to re-establish connections. target.pedrpc_connect() - - #################################################################################################################### - def server_init (self): - ''' + def server_init(self): + """ Called by fuzz() on first run (not on recursive re-entry) to initialize variables, web interface, etc... - ''' - - self.total_mutant_index = 0 + """ + self.total_mutant_index = 0 self.total_num_mutations = self.num_mutations() # web interface thread doesn't catch KeyboardInterrupt # add a signal handler, and exit on SIGINT # TODO: should wait for the end of the ongoing test case, and stop gracefully netmon and procmon # TODO: doesn't work on OS where the signal module isn't available - try: - import signal - self.signal_module = True - except: - self.signal_module = False + if self.signal_module: - def exit_abruptly(signal, frame): - '''Save current settings (just in case) and exit''' + # noinspection PyUnusedLocal + def exit_abruptly(signal_recv, frame_recv): + """ + Save current settings (just in case) and exit + """ self.export_file() self.logger.critical("SIGINT received ... exiting") - try: - self.thread.join() - except: - self.logger.debug( "No server launched") - sys.exit(0) + signal.signal(signal.SIGINT, exit_abruptly) # spawn the web interface. - self.thread = web_interface_thread(self) - self.thread.start() - + self.web_interface_thread.start() - #################################################################################################################### - def transmit (self, sock, node, edge, target): - ''' + def transmit(self, sock, node, edge): + """ Render and transmit a node, process callbacks accordingly. - @type sock: Socket + @type sock: socket.socket @param sock: Socket to transmit node on - @type node: Request (Node) + @type node: pgraph.node.node (Node) @param node: Request/Node to transmit - @type edge: Connection (pgraph.edge) + @type edge: pgraph.edge.edge (pgraph.edge) @param edge: Edge along the current fuzz path from "node" to next node. - @type target: session.target - @param target: Target we are transmitting to - ''' + """ data = None @@ -843,335 +796,51 @@ def transmit (self, sock, node, edge, target): if edge.callback: data = edge.callback(self, node, edge, sock) - self.logger.info("xmitting: [%d.%d]" % (node.id, self.total_mutant_index)) + self.logger.info("Transmitting: [%d.%d]" % (node.id, self.total_mutant_index)) # if no data was returned by the callback, render the node here. if not data: data = node.render() - # if data length is > 65507 and proto is UDP, truncate it. - # TODO: this logic does not prevent duplicate test cases, need to address this in the future. - if self.proto == socket.SOCK_DGRAM: - # max UDP packet size. - # TODO: anyone know how to determine this value smarter? - # - See http://stackoverflow.com/questions/25841/maximum-buffer-length-for-sendto to fix this - MAX_UDP = 65507 - - if os.name != "nt" and os.uname()[0] == "Darwin": - MAX_UDP = 9216 - - if len(data) > MAX_UDP: - self.logger.debug("Too much data for UDP, truncating to %d bytes" % MAX_UDP) - data = data[:MAX_UDP] - + # Try to send payload down-range try: + # TCP/SSL if self.proto == socket.SOCK_STREAM: sock.send(data) - else: + # UDP + elif self.proto == socket.SOCK_DGRAM: + # TODO: this logic does not prevent duplicate test cases, need to address this in the future. + # If our data is over the max UDP size for this platform, truncate before sending + if len(data) > self.max_udp: + self.logger.debug("Too much data for UDP, truncating to %d bytes" % self.max_udp) + data = data[:self.max_udp] + sock.sendto(data, (self.targets[0].host, self.targets[0].port)) + self.logger.debug("Packet sent : " + repr(data)) + + # Receive data + # TODO: Remove magic number (10000) + self.last_recv = sock.recv(10000) + except Exception, inst: self.logger.error("Socket error, send: %s" % inst) - if self.proto == (socket.SOCK_STREAM or socket.SOCK_DGRAM): - # TODO: might have a need to increase this at some point. (possibly make it a class parameter) - try: - self.last_recv = sock.recv(10000) - except Exception, e: - self.last_recv = "" - else: - self.last_recv = "" - - if len(self.last_recv) > 0: + # If we have data in our recv buffer + if self.last_recv: self.logger.debug("received: [%d] %s" % (len(self.last_recv), repr(self.last_recv))) + # Assume a crash? else: self.logger.warning("Nothing received on socket.") # Increment individual crash count - self.crashing_primitives[self.fuzz_node.mutant] = self.crashing_primitives.get(self.fuzz_node.mutant,0) +1 + self.crashing_primitives[self.fuzz_node.mutant] = self.crashing_primitives.get(self.fuzz_node.mutant, 0) + 1 # Note crash information - self.protmon_results[self.total_mutant_index] = data ; - #print self.protmon_results - - - -######################################################################################################################## -class web_interface_handler (BaseHTTPServer.BaseHTTPRequestHandler): - def __init__(self, request, client_address, server): - BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, client_address, server) - self.session = None - - - def commify (self, number): - number = str(number) - processing = 1 - regex = re.compile(r"^(-?\d+)(\d{3})") - - while processing: - (number, processing) = regex.subn(r"\1,\2",number) - - return number - - - def do_GET (self): - self.do_everything() - - - def do_HEAD (self): - self.do_everything() - - - def do_POST (self): - self.do_everything() - - - def do_everything (self): - if "pause" in self.path: - self.session.pause_flag = True - - if "resume" in self.path: - self.session.pause_flag = False - - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - - if "view_crash" in self.path: - response = self.view_crash(self.path) - elif "view_pcap" in self.path: - response = self.view_pcap(self.path) - else: - response = self.view_index() - - self.wfile.write(response) - - - def log_error (self, *args, **kwargs): - pass - - - def log_message (self, *args, **kwargs): - pass - - - def version_string (self): - return "Sulley Fuzz Session" - - - def view_crash (self, path): - test_number = int(path.split("/")[-1]) - return "
%s
" % self.session.procmon_results[test_number] - - - def view_pcap (self, path): - return path - - - def view_index (self): - response = """ - - - - Sulley Fuzz Control - - - -
-
- - - - - - - - - - - - - - -
Sulley Fuzz Control
%(status)s
- - - - - - - - - - - - - - - - - -
Total:%(total_mutant_index)sof%(total_num_mutations)s%(progress_total_bar)s%(progress_total)s
%(current_name)s:%(current_mutant_index)sof%(current_num_mutations)s%(progress_current_bar)s%(progress_current)s
-
-
- -
-
-
- -
-
- - - - - - - - - """ - - keys = self.session.procmon_results.keys() - keys.sort() - for key in keys: - val = self.session.procmon_results[key] - bytes = " " - - if self.session.netmon_results.has_key(key): - bytes = self.commify(self.session.netmon_results[key]) - - response += '' % (key, key, val.split("\n")[0], bytes) - - response += """ - -
Test Case #Crash SynopsisCaptured Bytes
%06d%s%s
- - -
-
- - - """ - - # what is the fuzzing status. - if self.session.pause_flag: - status = "PAUSED" - else: - status = "RUNNING" - - # if there is a current fuzz node. - if self.session.fuzz_node: - # which node (request) are we currently fuzzing. - if self.session.fuzz_node.name: - current_name = self.session.fuzz_node.name - else: - current_name = "[N/A]" - - # render sweet progress bars. - progress_current = float(self.session.fuzz_node.mutant_index) / float(self.session.fuzz_node.num_mutations()) - num_bars = int(progress_current * 50) - progress_current_bar = "[" + "=" * num_bars + " " * (50 - num_bars) + "]" - progress_current = "%.3f%%" % (progress_current * 100) - - progress_total = float(self.session.total_mutant_index) / float(self.session.total_num_mutations) - num_bars = int(progress_total * 50) - progress_total_bar = "[" + "=" * num_bars + " " * (50 - num_bars) + "]" - progress_total = "%.3f%%" % (progress_total * 100) - - response %= \ - { - "current_mutant_index" : self.commify(self.session.fuzz_node.mutant_index), - "current_name" : current_name, - "current_num_mutations" : self.commify(self.session.fuzz_node.num_mutations()), - "progress_current" : progress_current, - "progress_current_bar" : progress_current_bar, - "progress_total" : progress_total, - "progress_total_bar" : progress_total_bar, - "status" : status, - "total_mutant_index" : self.commify(self.session.total_mutant_index), - "total_num_mutations" : self.commify(self.session.total_num_mutations), - } - else: - response %= \ - { - "current_mutant_index" : "", - "current_name" : "", - "current_num_mutations" : "", - "progress_current" : "", - "progress_current_bar" : "", - "progress_total" : "", - "progress_total_bar" : "", - "status" : "UNAVAILABLE", - "total_mutant_index" : "", - "total_num_mutations" : "", - } - - return response - - -######################################################################################################################## -class web_interface_server (BaseHTTPServer.HTTPServer): - ''' - http://docs.python.org/lib/module-BaseHTTPServer.html - ''' - - def __init__(self, server_address, RequestHandlerClass, session): - BaseHTTPServer.HTTPServer.__init__(self, server_address, RequestHandlerClass) - self.RequestHandlerClass.session = session - - -######################################################################################################################## -class web_interface_thread (threading.Thread): - def __init__ (self, session): - threading.Thread.__init__(self, name="SulleyWebServer") - - self._stopevent = threading.Event() - self.session = session - self.server = None - - - def run (self): - self.server = web_interface_server(('', self.session.web_port), web_interface_handler, self.session) - while not self._stopevent.isSet(): - self.server.handle_request() - - def join(self, timeout=None): - # A little dirty but no other solution afaik - self._stopevent.set() - conn = httplib.HTTPConnection("localhost:%d" % self.session.web_port) - conn.request("GET", "/") - conn.getresponse() + self.protmon_results[self.total_mutant_index] = data + + def build_webapp_thread(self, port=26000): + app.session = self + http_server = HTTPServer(WSGIContainer(app)) + http_server.listen(port) + flask_thread = threading.Thread(target=IOLoop.instance().start) + flask_thread.daemon = True + return flask_thread diff --git a/sulley/sex.py b/sulley/sex.py index 774809e..b42446f 100644 --- a/sulley/sex.py +++ b/sulley/sex.py @@ -1,4 +1,12 @@ # Sulley EXception Class + class SullyRuntimeError(Exception): + pass + + +class SizerNotUtilizedError(Exception): + pass + +class MustImplementException(Exception): pass \ No newline at end of file diff --git a/sulley/utils/__init__.py b/sulley/utils/__init__.py index 29dbf24..ac2776f 100644 --- a/sulley/utils/__init__.py +++ b/sulley/utils/__init__.py @@ -1,3 +1,2 @@ import dcerpc -import misc import scada diff --git a/sulley/utils/dcerpc.py b/sulley/utils/dcerpc.py index a8073d2..768b226 100644 --- a/sulley/utils/dcerpc.py +++ b/sulley/utils/dcerpc.py @@ -1,49 +1,48 @@ import math import struct -import misc +from sulley import helpers -######################################################################################################################## -def bind (uuid, version): - ''' + +def bind(uuid, version): + """ Generate the data necessary to bind to the specified interface. - ''' + """ major, minor = version.split(".") major = struct.pack("> 1) ^ 0xa001 # polly - else: crc >>= 1 - - byte >>= 1 - - crc16_table.append(crc) - - for ch in string: - value = crc16_table[ord(ch) ^ (value & 0xff)] ^ (value >> 8) - - return value - - -######################################################################################################################## -def uuid_bin_to_str (uuid): - ''' - Convert a binary UUID to human readable string. - ''' - - (block1, block2, block3) = struct.unpack("HHL", uuid[8:16]) - - return "%08x-%04x-%04x-%04x-%04x%08x" % (block1, block2, block3, block4, block5, block6) - - -######################################################################################################################## -def uuid_str_to_bin (uuid): - ''' - Ripped from Core Impacket. Converts a UUID string to binary form. - ''' - - matches = re.match('([\dA-Fa-f]{8})-([\dA-Fa-f]{4})-([\dA-Fa-f]{4})-([\dA-Fa-f]{4})-([\dA-Fa-f]{4})([\dA-Fa-f]{8})', uuid) - - (uuid1, uuid2, uuid3, uuid4, uuid5, uuid6) = map(lambda x: long(x, 16), matches.groups()) - - uuid = struct.pack('HHL', uuid4, uuid5, uuid6) - - return uuid diff --git a/sulley/utils/scada.py b/sulley/utils/scada.py index 1c0684b..0f52830 100644 --- a/sulley/utils/scada.py +++ b/sulley/utils/scada.py @@ -1,17 +1,17 @@ import math import struct +from sulley.helpers import crc16 -######################################################################################################################## -def dnp3 (data, control_code="\x44", src="\x00\x00", dst="\x00\x00"): +def dnp3(data, control_code="\x44", src="\x00\x00", dst="\x00\x00"): num_packets = int(math.ceil(float(len(data)) / 250.0)) packets = [] for i in xrange(num_packets): - slice = data[i*250 : (i+1)*250] + packet_slice = data[i * 250:(i + 1) * 250] p = "\x05\x64" - p += chr(len(slice)) + p += chr(len(packet_slice)) p += control_code p += dst p += src @@ -20,7 +20,7 @@ def dnp3 (data, control_code="\x44", src="\x00\x00", dst="\x00\x00"): p += chksum - num_chunks = int(math.ceil(float(len(slice) / 16.0))) + num_chunks = int(math.ceil(float(len(packet_slice) / 16.0))) # insert the fragmentation flags / sequence number. # first frag: 0x40, last frag: 0x80 @@ -36,7 +36,7 @@ def dnp3 (data, control_code="\x44", src="\x00\x00", dst="\x00\x00"): p += chr(frag_number) for x in xrange(num_chunks): - chunk = slice[i*16 : (i+1)*16] + chunk = packet_slice[i * 16: (i + 1) * 16] chksum = struct.pack("", name="delim", fuzzable=False) @@ -103,28 +111,27 @@ def repeaters (): s_block_end() s_repeat("BLOCK", min_reps=5, max_reps=15, step=5) - data = s_render() + data = s_render() length = len(data) s_mutate() data = s_render() - assert(len(data) == length + length * 5) + assert (len(data) == length + length * 5) s_mutate() data = s_render() - assert(len(data) == length + length * 10) + assert (len(data) == length + length * 10) s_mutate() data = s_render() - assert(len(data) == length + length * 15) + assert (len(data) == length + length * 15) s_mutate() data = s_render() - assert(len(data) == length) + assert (len(data) == length) -######################################################################################################################## -def return_current_mutant (): +def return_current_mutant(): s_initialize("RETURN CURRENT MUTANT TEST 1") s_dword(0xdeadbeef, name="boss hog") @@ -150,46 +157,43 @@ def return_current_mutant (): for i in xrange(1, num_str_mutations + num_int_mutations - 10 + 1): req1.mutate() - assert(req1.mutant.name == "vagina") + assert (req1.mutant.name == "vagina") req1.reset() for i in xrange(1, num_int_mutations + num_str_mutations + 1 + 1): req1.mutate() - assert(req1.mutant.name == "foo") + assert (req1.mutant.name == "foo") req1.reset() for i in xrange(num_str_mutations * 2 + num_int_mutations + 1): req1.mutate() - assert(req1.mutant.name == "bar") + assert (req1.mutant.name == "bar") req1.reset() for i in xrange(num_str_mutations * 3 + num_int_mutations * 4 + 1): req1.mutate() - assert(req1.mutant.name == "uhntiss") + assert (req1.mutant.name == "uhntiss") req1.reset() -######################################################################################################################## -def exhaustion (): - +def exhaustion(): s_initialize("EXHAUSTION 1") s_string("just wont eat", name="VIP") s_dword(0x4141, name="eggos_rule") s_dword(0x4242, name="danny_glover_is_the_man") - req1 = s_get("EXHAUSTION 1") num_str_mutations = req1.names["VIP"].num_mutations() # if we mutate string halfway, then exhaust, then mutate one time, we should be in the 2nd primitive - for i in xrange(num_str_mutations/2): + for i in xrange(num_str_mutations / 2): req1.mutate() req1.mutant.exhaust() req1.mutate() - assert(req1.mutant.name == "eggos_rule") + assert (req1.mutant.name == "eggos_rule") req1.reset() # if we mutate through the first primitive, then exhaust the 2nd, we should be in the 3rd @@ -198,11 +202,11 @@ def exhaustion (): req1.mutant.exhaust() req1.mutate() - assert(req1.mutant.name == "danny_glover_is_the_man") + assert (req1.mutant.name == "danny_glover_is_the_man") req1.reset() # if we exhaust the first two primitives, we should be in the third req1.mutant.exhaust() req1.mutant.exhaust() - assert(req1.mutant.name == "danny_glover_is_the_man") + assert (req1.mutant.name == "danny_glover_is_the_man") diff --git a/unit_tests/legos.py b/unit_tests/legos.py index c434cac..6ecc6d0 100644 --- a/unit_tests/legos.py +++ b/unit_tests/legos.py @@ -1,17 +1,17 @@ from sulley import * -def run (): + +def run(): tag() ndr_string() ber() # clear out the requests. blocks.REQUESTS = {} - blocks.CURRENT = None + blocks.CURRENT = None -######################################################################################################################## -def tag (): +def tag(): s_initialize("UNIT TEST TAG 1") s_lego("tag", value="pedram") @@ -21,30 +21,28 @@ def tag (): print "\ttag: %d" % req.num_mutations() -######################################################################################################################## -def ndr_string (): +def ndr_string(): s_initialize("UNIT TEST NDR 1") s_lego("ndr_string", value="pedram") req = s_get("UNIT TEST NDR 1") # TODO: unfinished! - #print req.render() + # print req.render() -######################################################################################################################## -def ber (): +def ber(): s_initialize("UNIT TEST BER 1") s_lego("ber_string", value="pedram") req = s_get("UNIT TEST BER 1") - assert(s_render() == "\x04\x84\x00\x00\x00\x06\x70\x65\x64\x72\x61\x6d") + assert (s_render() == "\x04\x84\x00\x00\x00\x06\x70\x65\x64\x72\x61\x6d") s_mutate() - assert(s_render() == "\x04\x84\x00\x00\x00\x00\x70\x65\x64\x72\x61\x6d") + assert (s_render() == "\x04\x84\x00\x00\x00\x00\x70\x65\x64\x72\x61\x6d") s_initialize("UNIT TEST BER 2") s_lego("ber_integer", value=0xdeadbeef) req = s_get("UNIT TEST BER 2") - assert(s_render() == "\x02\x04\xde\xad\xbe\xef") + assert (s_render() == "\x02\x04\xde\xad\xbe\xef") s_mutate() - assert(s_render() == "\x02\x04\x00\x00\x00\x00") + assert (s_render() == "\x02\x04\x00\x00\x00\x00") s_mutate() - assert(s_render() == "\x02\x04\x00\x00\x00\x01") \ No newline at end of file + assert (s_render() == "\x02\x04\x00\x00\x00\x01") \ No newline at end of file diff --git a/unit_tests/primitives.py b/unit_tests/primitives.py index 54709da..7e16780 100644 --- a/unit_tests/primitives.py +++ b/unit_tests/primitives.py @@ -1,81 +1,79 @@ from sulley import * -def run (): + +def run(): signed_tests() string_tests() fuzz_extension_tests() # clear out the requests. blocks.REQUESTS = {} - blocks.CURRENT = None + blocks.CURRENT = None -######################################################################################################################## -def signed_tests (): +def signed_tests(): s_initialize("UNIT TEST 1") - s_byte(0, format="ascii", signed=True, name="byte_1") - s_byte(0xff/2, format="ascii", signed=True, name="byte_2") - s_byte(0xff/2+1, format="ascii", signed=True, name="byte_3") - s_byte(0xff, format="ascii", signed=True, name="byte_4") - - s_word(0, format="ascii", signed=True, name="word_1") - s_word(0xffff/2, format="ascii", signed=True, name="word_2") - s_word(0xffff/2+1, format="ascii", signed=True, name="word_3") - s_word(0xffff, format="ascii", signed=True, name="word_4") - - s_dword(0, format="ascii", signed=True, name="dword_1") - s_dword(0xffffffff/2, format="ascii", signed=True, name="dword_2") - s_dword(0xffffffff/2+1, format="ascii", signed=True, name="dword_3") - s_dword(0xffffffff, format="ascii", signed=True, name="dword_4") - - s_qword(0, format="ascii", signed=True, name="qword_1") - s_qword(0xffffffffffffffff/2, format="ascii", signed=True, name="qword_2") - s_qword(0xffffffffffffffff/2+1, format="ascii", signed=True, name="qword_3") - s_qword(0xffffffffffffffff, format="ascii", signed=True, name="qword_4") + s_byte(0, output_format="ascii", signed=True, name="byte_1") + s_byte(0xff / 2, output_format="ascii", signed=True, name="byte_2") + s_byte(0xff / 2 + 1, output_format="ascii", signed=True, name="byte_3") + s_byte(0xff, output_format="ascii", signed=True, name="byte_4") + + s_word(0, output_format="ascii", signed=True, name="word_1") + s_word(0xffff / 2, output_format="ascii", signed=True, name="word_2") + s_word(0xffff / 2 + 1, output_format="ascii", signed=True, name="word_3") + s_word(0xffff, output_format="ascii", signed=True, name="word_4") + + s_dword(0, output_format="ascii", signed=True, name="dword_1") + s_dword(0xffffffff / 2, output_format="ascii", signed=True, name="dword_2") + s_dword(0xffffffff / 2 + 1, output_format="ascii", signed=True, name="dword_3") + s_dword(0xffffffff, output_format="ascii", signed=True, name="dword_4") + + s_qword(0, output_format="ascii", signed=True, name="qword_1") + s_qword(0xffffffffffffffff / 2, output_format="ascii", signed=True, name="qword_2") + s_qword(0xffffffffffffffff / 2 + 1, output_format="ascii", signed=True, name="qword_3") + s_qword(0xffffffffffffffff, output_format="ascii", signed=True, name="qword_4") req = s_get("UNIT TEST 1") - assert(req.names["byte_1"].render() == "0") - assert(req.names["byte_2"].render() == "127") - assert(req.names["byte_3"].render() == "-128") - assert(req.names["byte_4"].render() == "-1") - assert(req.names["word_1"].render() == "0") - assert(req.names["word_2"].render() == "32767") - assert(req.names["word_3"].render() == "-32768") - assert(req.names["word_4"].render() == "-1") - assert(req.names["dword_1"].render() == "0") - assert(req.names["dword_2"].render() == "2147483647") - assert(req.names["dword_3"].render() == "-2147483648") - assert(req.names["dword_4"].render() == "-1") - assert(req.names["qword_1"].render() == "0") - assert(req.names["qword_2"].render() == "9223372036854775807") - assert(req.names["qword_3"].render() == "-9223372036854775808") - assert(req.names["qword_4"].render() == "-1") - - -######################################################################################################################## -def string_tests (): + assert (req.names["byte_1"].render() == "0") + assert (req.names["byte_2"].render() == "127") + assert (req.names["byte_3"].render() == "-128") + assert (req.names["byte_4"].render() == "-1") + assert (req.names["word_1"].render() == "0") + assert (req.names["word_2"].render() == "32767") + assert (req.names["word_3"].render() == "-32768") + assert (req.names["word_4"].render() == "-1") + assert (req.names["dword_1"].render() == "0") + assert (req.names["dword_2"].render() == "2147483647") + assert (req.names["dword_3"].render() == "-2147483648") + assert (req.names["dword_4"].render() == "-1") + assert (req.names["qword_1"].render() == "0") + assert (req.names["qword_2"].render() == "9223372036854775807") + assert (req.names["qword_3"].render() == "-9223372036854775808") + assert (req.names["qword_4"].render() == "-1") + + +def string_tests(): s_initialize("STRING UNIT TEST 1") s_string("foo", size=200, name="sized_string") req = s_get("STRING UNIT TEST 1") - assert(len(req.names["sized_string"].render()) == 3) + assert (len(req.names["sized_string"].render()) == 3) # check that string padding and truncation are working correctly. for i in xrange(0, 50): s_mutate() - assert(len(req.names["sized_string"].render()) == 200) + assert (len(req.names["sized_string"].render()) == 200) -######################################################################################################################## -def fuzz_extension_tests (): +def fuzz_extension_tests(): import shutil # backup existing fuzz extension libraries. try: shutil.move(".fuzz_strings", ".fuzz_strings_backup") - shutil.move(".fuzz_ints", ".fuzz_ints_backup") + shutil.move(".fuzz_ints", ".fuzz_ints_backup") except: pass @@ -93,26 +91,26 @@ def fuzz_extension_tests (): s_initialize("EXTENSION TEST") s_string("foo", name="string") - s_int(200, name="int") - s_char("A", name="char") + s_int(200, name="int") + s_char("A", name="char") req = s_get("EXTENSION TEST") # these should be here now. - assert(0xdeadbeef in req.names["int"].fuzz_library) - assert(0xc0cac01a in req.names["int"].fuzz_library) + assert (0xdeadbeef in req.names["int"].fuzz_library) + assert (0xc0cac01a in req.names["int"].fuzz_library) # these should not as a char is too small to store them. - assert(0xdeadbeef not in req.names["char"].fuzz_library) - assert(0xc0cac01a not in req.names["char"].fuzz_library) + assert (0xdeadbeef not in req.names["char"].fuzz_library) + assert (0xc0cac01a not in req.names["char"].fuzz_library) # these should be here now. - assert("pedram" in req.names["string"].fuzz_library) - assert("amini" in req.names["string"].fuzz_library) + assert ("pedram" in req.names["string"].fuzz_library) + assert ("amini" in req.names["string"].fuzz_library) # restore existing fuzz extension libraries. try: shutil.move(".fuzz_strings_backup", ".fuzz_strings") - shutil.move(".fuzz_ints_backup", ".fuzz_ints") + shutil.move(".fuzz_ints_backup", ".fuzz_ints") except: pass \ No newline at end of file diff --git a/utils/crash_binning.py b/utils/crash_binning.py index f52d539..4af9bab 100644 --- a/utils/crash_binning.py +++ b/utils/crash_binning.py @@ -15,54 +15,49 @@ # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -''' +""" @author: Pedram Amini @license: GNU General Public License 2.0 or later @contact: pedram.amini@gmail.com @organization: www.openrce.org -''' +""" -import sys import zlib import cPickle -class __crash_bin_struct__: - exception_module = None - exception_address = 0 - write_violation = 0 - violation_address = 0 - violation_thread_id = 0 - context = None - context_dump = None - disasm = None - disasm_around = [] - stack_unwind = [] - seh_unwind = [] - extra = None - - -class crash_binning: - ''' + +class CrashBinStruct: + def __init__(self): + self.exception_module = None + self.exception_address = 0 + self.write_violation = 0 + self.violation_address = 0 + self.violation_thread_id = 0 + self.context = None + self.context_dump = None + self.disasm = None + self.disasm_around = [] + self.stack_unwind = [] + self.seh_unwind = [] + self.extra = None + + +class CrashBinning: + """ @todo: Add MySQL import/export. - ''' + """ bins = {} last_crash = None pydbg = None - #################################################################################################################### - def __init__ (self): - ''' - ''' - + def __init__(self): self.bins = {} self.last_crash = None self.pydbg = None - - #################################################################################################################### - def record_crash (self, pydbg, extra=None): - ''' + def record_crash(self, pydbg, extra=None): + """ Given a PyDbg instantiation that at the current time is assumed to have "crashed" (access violation for example) record various details such as the disassemly around the violating address, the ID of the offending thread, the call stack and the SEH unwind. Store the recorded data in an internal dictionary, binning them by the exception @@ -72,10 +67,10 @@ def record_crash (self, pydbg, extra=None): @param pydbg: Instance of pydbg @type extra: Mixed @param extra: (Optional, Def=None) Whatever extra data you want to store with this bin - ''' + """ self.pydbg = pydbg - crash = __crash_bin_struct__() + crash = CrashBinStruct() # add module name to the exception address. exception_module = pydbg.addr_to_module(pydbg.dbg.u.Exception.ExceptionRecord.ExceptionAddress) @@ -110,7 +105,6 @@ def record_crash (self, pydbg, extra=None): crash.stack_unwind[i] = "%s:%08x" % (module, addr) - # add module names to the SEH unwind. for i in xrange(len(crash.seh_unwind)): (addr, handler) = crash.seh_unwind[i] @@ -124,28 +118,26 @@ def record_crash (self, pydbg, extra=None): crash.seh_unwind[i] = (addr, handler, "%s:%08x" % (module, handler)) - if not self.bins.has_key(crash.exception_address): + if not crash.exception_address in self.bins: self.bins[crash.exception_address] = [] self.bins[crash.exception_address].append(crash) self.last_crash = crash - - #################################################################################################################### - def crash_synopsis (self, crash=None): - ''' + def crash_synopsis(self, crash=None): + """ For the supplied crash, generate and return a report containing the disassemly around the violating address, the ID of the offending thread, the call stack and the SEH unwind. If not crash is specified, then call through to last_crash_synopsis() which returns the same information for the last recorded crash. @see: crash_synopsis() - @type crash: __crash_bin_struct__ + @type crash: CrashBinStruct @param crash: (Optional, def=None) Crash object to generate report on - @rtype: String + @rtype: str @return: Crash report - ''' + """ if not crash: return self.last_crash_synopsis() @@ -157,12 +149,12 @@ def crash_synopsis (self, crash=None): synopsis = "%s:%08x %s from thread %d caused access violation\nwhen attempting to %s 0x%08x\n\n" % \ ( - crash.exception_module, \ - crash.exception_address, \ - crash.disasm, \ - crash.violation_thread_id, \ - direction, \ - crash.violation_address \ + crash.exception_module, + crash.exception_address, + crash.disasm, + crash.violation_thread_id, + direction, + crash.violation_address ) synopsis += crash.context_dump @@ -179,14 +171,12 @@ def crash_synopsis (self, crash=None): if len(crash.seh_unwind): synopsis += "\nSEH unwind:\n" for (addr, handler, handler_str) in crash.seh_unwind: - synopsis += "\t%08x -> %s\n" % (addr, handler_str) + synopsis += "\t%08x -> %s\n" % (addr, handler_str) return synopsis + "\n" - - #################################################################################################################### - def export_file (self, file_name): - ''' + def export_file(self, file_name): + """ Dump the entire object structure to disk. @see: import_file() @@ -194,9 +184,9 @@ def export_file (self, file_name): @type file_name: str @param file_name: File name to export to - @rtype: crash_binning + @rtype: CrashBinning @return: self - ''' + """ # null out what we don't serialize but save copies to restore after dumping to disk. last_crash = self.last_crash @@ -213,10 +203,8 @@ def export_file (self, file_name): return self - - #################################################################################################################### - def import_file (self, file_name): - ''' + def import_file(self, file_name): + """ Load the entire object structure from disk. @see: export_file() @@ -224,9 +212,9 @@ def import_file (self, file_name): @type file_name: str @param file_name: File name to import from - @rtype: crash_binning + @rtype: CrashBinning @return: self - ''' + """ fh = open(file_name, "rb") tmp = cPickle.loads(zlib.decompress(fh.read())) @@ -236,10 +224,8 @@ def import_file (self, file_name): return self - - #################################################################################################################### - def last_crash_synopsis (self): - ''' + def last_crash_synopsis(self): + """ For the last recorded crash, generate and return a report containing the disassemly around the violating address, the ID of the offending thread, the call stack and the SEH unwind. @@ -247,7 +233,7 @@ def last_crash_synopsis (self): @rtype: String @return: Crash report - ''' + """ if self.last_crash.write_violation: direction = "write to" @@ -256,12 +242,12 @@ def last_crash_synopsis (self): synopsis = "%s:%08x %s from thread %d caused access violation\nwhen attempting to %s 0x%08x\n\n" % \ ( - self.last_crash.exception_module, \ - self.last_crash.exception_address, \ - self.last_crash.disasm, \ - self.last_crash.violation_thread_id, \ - direction, \ - self.last_crash.violation_address \ + self.last_crash.exception_module, + self.last_crash.exception_address, + self.last_crash.disasm, + self.last_crash.violation_thread_id, + direction, + self.last_crash.violation_address ) synopsis += self.last_crash.context_dump @@ -283,6 +269,6 @@ def last_crash_synopsis (self): except: disasm = "[INVALID]" - synopsis += "\t%08x -> %s %s\n" % (addr, handler_str, disasm) + synopsis += "\t%08x -> %s %s\n" % (addr, handler_str, disasm) return synopsis + "\n" \ No newline at end of file diff --git a/utils/crashbin_explorer.py b/utils/crashbin_explorer.py index b940ed6..66276db 100644 --- a/utils/crashbin_explorer.py +++ b/utils/crashbin_explorer.py @@ -27,11 +27,13 @@ test_number = graph_name = graph = None for opt, arg in opts: - if opt in ("-t", "--test"): test_number = int(arg) - if opt in ("-g", "--graph"): graph_name = arg + if opt in ("-t", "--test"): + test_number = int(arg) + if opt in ("-g", "--graph"): + graph_name = arg try: - crashbin = utils.crash_binning.crash_binning() + crashbin = utils.crash_binning.CrashBinning() crashbin.import_file(sys.argv[1]) except: print "unable to open crashbin: '%s'." % sys.argv[1] @@ -42,7 +44,7 @@ # if test_number: - for bin, crashes in crashbin.bins.iteritems(): + for _, crashes in crashbin.bins.iteritems(): for crash in crashes: if test_number == crash.extra: print crashbin.crash_synopsis(crash) @@ -53,29 +55,27 @@ # if graph_name: - graph = pgraph.graph() + graph = pgraph.Graph() -for bin, crashes in crashbin.bins.iteritems(): +# noinspection PyRedeclaration +for _, crashes in crashbin.bins.iteritems(): synopsis = crashbin.crash_synopsis(crashes[0]).split("\n")[0] - if graph: - crash_node = pgraph.node(crashes[0].exception_address) - crash_node.count = len(crashes) - crash_node.label = "[%d] %s.%08x" % (crash_node.count, crashes[0].exception_module, crash_node.id) - graph.add_node(crash_node) - - print "[%d] %s" % (len(crashes), synopsis) - print "\t", - for crash in crashes: if graph: + crash_node = pgraph.Node(crashes[0].exception_address) + crash_node.count = len(crashes) + crash_node.label = "[%d] %s.%08x" % (crash_node.count, crashes[0].exception_module, crash_node.id) + graph.add_node(crash_node) + print "[%d] %s" % (len(crashes), synopsis) + print "\t", last = crash_node.id for entry in crash.stack_unwind: address = long(entry.split(":")[1], 16) n = graph.find_node("id", address) if not n: - n = pgraph.node(address) + n = pgraph.Node(address) n.count = 1 n.label = "[%d] %s" % (n.count, entry) graph.add_node(n) @@ -83,7 +83,7 @@ n.count += 1 n.label = "[%d] %s" % (n.count, entry) - edge = pgraph.edge(n.id, last) + edge = pgraph.Edge(n.id, last) graph.add_edge(edge) last = n.id print "%d," % crash.extra, diff --git a/utils/ida_fuzz_library_extender.py b/utils/ida_fuzz_library_extender.py index 00aef68..c11cba8 100644 --- a/utils/ida_fuzz_library_extender.py +++ b/utils/ida_fuzz_library_extender.py @@ -5,8 +5,8 @@ # (C) 2007 # -######################################################################################################################## -def get_string( ea): + +def get_string(ea): str_type = GetStringType(ea) if str_type == 0: @@ -18,50 +18,49 @@ def get_string( ea): elif str_type == 3: string_buf = "" while Word(ea) != 0x0000: - string_buf += "%c%c" % (Byte(ea), Byte(ea+1)) + string_buf += "%c%c" % (Byte(ea), Byte(ea + 1)) ea += 2 return string_buf else: pass -######################################################################################################################## def get_arguments(ea): xref_ea = ea - args = 0 - found = None + args = 0 + found = None if GetMnem(xref_ea) != "call": return False cur_ea = PrevHead(ea, xref_ea - 32) while (cur_ea < xref_ea - 32) or (args <= 6): - cur_mnem = GetMnem(cur_ea); + cur_mnem = GetMnem(cur_ea) if cur_mnem == "push": args += 1 op_type = GetOpType(cur_ea, 0) if Comment(cur_ea): pass - #print(" %s = %s," % (Comment(cur_ea), GetOpnd(cur_ea, 0))) + # print(" %s = %s," % (Comment(cur_ea), GetOpnd(cur_ea, 0))) else: if op_type == 1: pass - #print(" %s" % GetOpnd(cur_ea, 0)) + # print(" %s" % GetOpnd(cur_ea, 0)) elif op_type == 5: found = get_string(GetOperandValue(cur_ea, 0)) elif cur_mnem == "call" or "j" in cur_mnem: - break; + break cur_ea = PrevHead(cur_ea, xref_ea - 32) - if found: return found + if found: + return found -######################################################################################################################## -def find_ints (start_address): - constants = [] +def find_ints(start_address): + constants = [] # loop heads for head in Heads(start_address, SegEnd(start_address)): @@ -78,15 +77,13 @@ def find_ints (start_address): print "Found %d constant values used in compares." % len(constants) print "-----------------------------------------------------" for i in xrange(0, len(constants), 20): - print constants[i:i+20] + print constants[i:i + 20] return constants -######################################################################################################################## -def find_strings (start_address): - strings = [] - string_arg = None +def find_strings(start_address): + strings = [] # do import checking import_ea = start_address @@ -96,12 +93,12 @@ def find_strings (start_address): if len(import_name) > 1 and "cmp" in import_name: xref_start = import_ea - xref_cur = DfirstB(xref_start) + xref_cur = DfirstB(xref_start) while xref_cur != BADADDR: - #print "Found call to ", import_name - string_arg = get_arguments(xref_cur) - + # print "Found call to ", import_name + string_arg = get_arguments(xref_cur) + if string_arg and string_arg not in strings: strings.append(string_arg) @@ -109,7 +106,6 @@ def find_strings (start_address): import_ea += 4 - # now do FLIRT checking for function_ea in Functions(SegByName(".text"), SegEnd(start_address)): flags = GetFunctionFlags(function_ea) @@ -121,7 +117,7 @@ def find_strings (start_address): # found one, now find xrefs to it and grab arguments xref_start = function_ea - xref_cur = RfirstB(xref_start) + xref_cur = RfirstB(xref_start) while xref_cur != BADADDR: string_arg = get_arguments(xref_cur) @@ -134,16 +130,14 @@ def find_strings (start_address): print "Found %d string values used in compares." % len(strings) print "-----------------------------------------------------" for i in xrange(0, len(strings), 5): - print strings[i:i+5] + print strings[i:i + 5] return strings -######################################################################################################################## - # get file names to save to constants_file = AskFile(1, ".fuzz_ints", "Enter filename for saving the discovered integers: ") -strings_file = AskFile(1, ".fuzz_strings", "Enter filename for saving the discovered strings: ") +strings_file = AskFile(1, ".fuzz_strings", "Enter filename for saving the discovered strings: ") # get integers start_address = SegByName(".text") diff --git a/utils/pcap_cleaner.py b/utils/pcap_cleaner.py index 18d177d..08dd038 100644 --- a/utils/pcap_cleaner.py +++ b/utils/pcap_cleaner.py @@ -18,14 +18,14 @@ # try: - crashbin = utils.crash_binning.crash_binning() + crashbin = utils.crash_binning.CrashBinning() crashbin.import_file(sys.argv[1]) except: print "unable to open crashbin: '%s'." % sys.argv[1] sys.exit(1) test_cases = [] -for bin, crashes in crashbin.bins.iteritems(): +for _, crashes in crashbin.bins.iteritems(): for crash in crashes: test_cases.append("%d.pcap" % crash.extra) diff --git a/utils/pdml_parser.py b/utils/pdml_parser.py index 94e3cb4..14d3f9c 100644 --- a/utils/pdml_parser.py +++ b/utils/pdml_parser.py @@ -7,105 +7,100 @@ from xml.sax.handler import feature_namespaces -######################################################################################################################## -class ParsePDML (ContentHandler): - - def __init__ (self): - self.current = None +class ParsePDML(ContentHandler): + def __init__(self): + ContentHandler.__init__(self) + self.current = None self.start_parsing = False - self.sulley = "" - - - def startElement (self, name, attributes): + self.sulley = "" + + def startElement(self, name, attributes): if name == "proto": self.current = attributes["name"] - + # if parsing flag is set, we're past tcp if self.start_parsing: - + if not name == "field": print "Found payload with name %s" % attributes["name"] elif name == "field": if "value" in attributes.keys(): val_string = self.get_string(attributes["value"]) - + if val_string: - self.sulley += "s_string(\"%s\")\n" % (val_string) + self.sulley += "s_string(\"%s\")\n" % val_string print self.sulley - #print "\tFound value: %s" % val_string + # print "\tFound value: %s" % val_string else: # not string pass else: - raise "WTFException" - - - def characters (self, data): + raise Exception("WTFException") + + def characters(self, data): pass - - - def endElement (self, name): + + def endElement(self, name): # if we're closing a packet if name == "packet": self.start_parsing = False - + # if we're closing a proto tag if name == "proto": # and that proto is tcp, set parsing flag if self.current == "tcp": - #print "Setting parsing flag to TRUE" + # print "Setting parsing flag to TRUE" self.start_parsing = True - + else: self.start_parsing = False - - + + # noinspection PyMethodMayBeStatic def get_string(self, parsed): - - parsed = parsed.replace("\t", "") - parsed = parsed.replace("\r", "") - parsed = parsed.replace("\n", "") - parsed = parsed.replace(",", "") - parsed = parsed.replace("0x", "") + + parsed = parsed.replace("\t", "") + parsed = parsed.replace("\r", "") + parsed = parsed.replace("\n", "") + parsed = parsed.replace(",", "") + parsed = parsed.replace("0x", "") parsed = parsed.replace("\\x", "") - + value = "" while parsed: - pair = parsed[:2] + pair = parsed[:2] parsed = parsed[2:] - - hex = int(pair, 16) - if hex > 0x7f: + + hex_pair = int(pair, 16) + if hex_pair > 0x7f: return False - - value += chr(hex) - - - value = value.replace("\t", "") - value = value.replace("\r", "") - value = value.replace("\n", "") - value = value.replace(",", "") - value = value.replace("0x", "") + + value += chr(hex_pair) + + value = value.replace("\t", "") + value = value.replace("\r", "") + value = value.replace("\n", "") + value = value.replace(",", "") + value = value.replace("0x", "") value = value.replace("\\x", "") - + return value - - def error (self, exception): + + # noinspection PyMethodMayBeStatic + def error(self, exception): print "Oh shitz: ", exception sys.exit(1) -######################################################################################################################## -if __name__ == '__main__': +if __name__ == '__main__': # create the parser object parser = make_parser() - + # dont care about xml namespace parser.setFeature(feature_namespaces, 0) # make the document handler handler = ParsePDML() - + # point parser to handler parser.setContentHandler(handler) diff --git a/utils/print_session.py b/utils/print_session.py index 5fc7147..3333b0d 100644 --- a/utils/print_session.py +++ b/utils/print_session.py @@ -1,6 +1,5 @@ #! /usr/bin/python -import os import sys import zlib import cPickle diff --git a/vmcontrol.py b/vmcontrol.py index 98d4190..564df69 100755 --- a/vmcontrol.py +++ b/vmcontrol.py @@ -6,8 +6,14 @@ import time import getopt +if os.name != "nt": + print "[!] This only works on windows!" + sys.exit(1) + try: + # noinspection PyUnresolvedReferences from win32api import GetShortPathName + # noinspection PyUnresolvedReferences from win32com.shell import shell except: if os.name == "nt": @@ -19,37 +25,37 @@ PORT = 26003 ERR = lambda msg: sys.stderr.write("ERR> " + msg + "\n") or sys.exit(1) -USAGE = "USAGE: vmcontrol.py" \ +USAGE = "USAGE: vmcontrol.py" \ "\n <-x|--vmx FILENAME|NAME> path to VMX to control or name of VirtualBox image" \ "\n <-r|--vmrun FILENAME> path to vmrun.exe or VBoxManage" \ "\n [-s|--snapshot NAME> set the snapshot name" \ "\n [-l|--log_level LEVEL] log level (default 1), increase for more verbosity" \ "\n [-i|--interactive] Interactive mode, prompts for input values" \ - "\n [--port PORT] TCP port to bind this agent to" \ + "\n [--port PORT] TCP port to bind this agent to" \ "\n [--vbox] control an Oracle VirtualBox VM" -######################################################################################################################## -class vmcontrol_pedrpc_server (pedrpc.server): - def __init__ (self, host, port, vmrun, vmx, snap_name=None, log_level=1, interactive=False): - ''' - @type host: String + +class VMControlPedrpcServer (pedrpc.Server): + def __init__(self, host, port, vmrun, vmx, snap_name=None, log_level=1, interactive=False): + """ + @type host: str @param host: Hostname or IP address to bind server to - @type port: Integer + @type port: int @param port: Port to bind server to - @type vmrun: String + @type vmrun: str @param vmrun: Path to VMWare vmrun.exe - @type vmx: String + @type vmx: str @param vmx: Path to VMX file - @type snap_name: String + @type snap_name: str @param snap_name: (Optional, def=None) Snapshot name to revert to on restart - @type log_level: Integer + @type log_level: int @param log_level: (Optional, def=1) Log output level, increase for more verbosity - @type interactive: Boolean + @type interactive: bool @param interactive: (Option, def=False) Interactive mode, prompts for input values - ''' + """ # initialize the PED-RPC server. - pedrpc.server.__init__(self, host, port) + pedrpc.Server.__init__(self, host, port) self.host = host self.port = port @@ -63,7 +69,11 @@ def __init__ (self, host, port, vmrun, vmx, snap_name=None, log_level=1, interac try: while 1: print "[*] Please browse to the folder containing vmrun.exe..." - pidl, disp, imglist = shell.SHBrowseForFolder(0, None, "Please browse to the folder containing vmrun.exe:") + pidl, disp, imglist = shell.SHBrowseForFolder( + 0, + None, + "Please browse to the folder containing vmrun.exe:" + ) fullpath = shell.SHGetPathFromIDList(pidl) file_list = os.listdir(fullpath) if "vmrun.exe" not in file_list: @@ -80,16 +90,20 @@ def __init__ (self, host, port, vmrun, vmx, snap_name=None, log_level=1, interac try: while 1: print "[*] Please browse to the folder containing the .vmx file..." - pidl, disp, imglist = shell.SHBrowseForFolder(0, None, "Please browse to the folder containing the .vmx file:") + pidl, disp, imglist = shell.SHBrowseForFolder( + 0, + None, + "Please browse to the folder containing the .vmx file:" + ) fullpath = shell.SHGetPathFromIDList(pidl) file_list = os.listdir(fullpath) exists = False - for file in file_list: - idx = file.find(".vmx") - if idx == len(file) - 4: + for filename in file_list: + idx = filename.find(".vmx") + if idx == len(filename) - 4: exists = True - vmx = fullpath + "\\" + file + vmx = fullpath + "\\" + filename print "[*] Using %s" % vmx if exists: @@ -97,7 +111,6 @@ def __init__ (self, host, port, vmrun, vmx, snap_name=None, log_level=1, interac else: print "[!] No .vmx file found in the selected folder, please try again" except: - raise print "[!] Error while trying to find the .vmx file. Try again without -I." sys.exit(1) @@ -130,49 +143,44 @@ def __init__ (self, host, port, vmrun, vmx, snap_name=None, log_level=1, interac self.log("\t log level: %d" % self.log_level) self.log("Awaiting requests...") - - def alive (self): - ''' + # noinspection PyMethodMayBeStatic + def alive(self): + """ Returns True. Useful for PED-RPC clients who want to see if the PED-RPC connection is still alive. - ''' - + """ return True - - def log (self, msg="", level=1): - ''' + def log(self, msg="", level=1): + """ If the supplied message falls under the current log level, print the specified message to screen. - @type msg: String + @type msg: str @param msg: Message to log - ''' + """ if self.log_level >= level: print "[%s] %s" % (time.strftime("%I:%M.%S"), msg) - - def set_vmrun (self, vmrun): + def set_vmrun(self, vmrun): self.log("setting vmrun to %s" % vmrun, 2) self.vmrun = vmrun - - def set_vmx (self, vmx): + def set_vmx(self, vmx): self.log("setting vmx to %s" % vmx, 2) self.vmx = vmx - - def set_snap_name (self, snap_name): + def set_snap_name(self, snap_name): self.log("setting snap_name to %s" % snap_name, 2) self.snap_name = snap_name - - def vmcommand (self, command): - ''' + def vmcommand(self, command): + """ Execute the specified command, keep trying in the event of a failure. - @type command: String + @type command: str @param command: VMRun command to execute - ''' + """ + out = None while 1: self.log("executing: %s" % command, 5) @@ -195,13 +203,7 @@ def vmcommand (self, command): return "".join(out) - - ### - ### VMRUN COMMAND WRAPPERS - ### - - - def delete_snapshot (self, snap_name=None): + def delete_snapshot(self, snap_name=None): if not snap_name: snap_name = self.snap_name @@ -210,29 +212,25 @@ def delete_snapshot (self, snap_name=None): command = self.vmrun + " deleteSnapshot " + self.vmx + " " + '"' + snap_name + '"' return self.vmcommand(command) - - def list (self): + def list(self): self.log("listing running images", 2) command = self.vmrun + " list" return self.vmcommand(command) - - def list_snapshots (self): + def list_snapshots(self): self.log("listing snapshots", 2) command = self.vmrun + " listSnapshots " + self.vmx return self.vmcommand(command) - - def reset (self): + def reset(self): self.log("resetting image", 2) command = self.vmrun + " reset " + self.vmx return self.vmcommand(command) - - def revert_to_snapshot (self, snap_name=None): + def revert_to_snapshot(self, snap_name=None): if not snap_name: snap_name = self.snap_name @@ -241,8 +239,7 @@ def revert_to_snapshot (self, snap_name=None): command = self.vmrun + " revertToSnapshot " + self.vmx + " " + '"' + snap_name + '"' return self.vmcommand(command) - - def snapshot (self, snap_name=None): + def snapshot(self, snap_name=None): if not snap_name: snap_name = self.snap_name @@ -252,34 +249,25 @@ def snapshot (self, snap_name=None): return self.vmcommand(command) - - def start (self): + def start(self): self.log("starting image", 2) command = self.vmrun + " start " + self.vmx return self.vmcommand(command) - - def stop (self): + def stop(self): self.log("stopping image", 2) command = self.vmrun + " stop " + self.vmx return self.vmcommand(command) - - def suspend (self): + def suspend(self): self.log("suspending image", 2) command = self.vmrun + " suspend " + self.vmx return self.vmcommand(command) - - ### - ### EXTENDED COMMANDS - ### - - - def restart_target (self): + def restart_target(self): self.log("restarting virtual machine...") # revert to the specified snapshot and start the image. @@ -289,8 +277,7 @@ def restart_target (self): # wait for the snapshot to come alive. self.wait() - - def is_target_running (self): + def is_target_running(self): # sometimes vmrun reports that the VM is up while it's still reverting. time.sleep(10) @@ -307,48 +294,42 @@ def is_target_running (self): return False - - def wait (self): + def wait(self): self.log("waiting for vmx to come up: %s" % self.vmx) while 1: if self.is_target_running(): break -######################################################################################################################## - -######################################################################################################################## -class vboxcontrol_pedrpc_server (vmcontrol_pedrpc_server): - def __init__ (self, host, port, vmrun, vmx, snap_name=None, log_level=1, interactive=False): - ''' +class VBoxControlPedrpcServer (VMControlPedrpcServer): + def __init__(self, host, port, vmrun, vmx, snap_name=None, log_level=1, interactive=False): + """ Controls an Oracle VirtualBox Virtual Machine - - @type host: String + + @type host: str @param host: Hostname or IP address to bind server to - @type port: Integer + @type port: int @param port: Port to bind server to - @type vmrun: String + @type vmrun: str @param vmrun: Path to VBoxManage - @type vmx: String + @type vmx: str @param vmx: Name of the virtualbox VM to control (no quotes) - @type snap_name: String + @type snap_name: str @param snap_name: (Optional, def=None) Snapshot name to revert to on restart - @type log_level: Integer + @type log_level: int @param log_level: (Optional, def=1) Log output level, increase for more verbosity - @type interactive: Boolean + @type interactive: bool @param interactive: (Option, def=False) Interactive mode, prompts for input values - ''' + """ # initialize the PED-RPC server. - pedrpc.server.__init__(self, host, port) + pedrpc.Server.__init__(self, host, port) self.host = host self.port = port self.interactive = interactive - - if interactive: print "[*] Entering interactive mode..." @@ -356,7 +337,11 @@ def __init__ (self, host, port, vmrun, vmx, snap_name=None, log_level=1, interac try: while 1: print "[*] Please browse to the folder containing VBoxManage.exe..." - pidl, disp, imglist = shell.SHBrowseForFolder(0, None, "Please browse to the folder containing VBoxManage.exe") + pidl, disp, imglist = shell.SHBrowseForFolder( + 0, + None, + "Please browse to the folder containing VBoxManage.exe" + ) fullpath = shell.SHGetPathFromIDList(pidl) file_list = os.listdir(fullpath) if "VBoxManage.exe" not in file_list: @@ -369,29 +354,22 @@ def __init__ (self, host, port, vmrun, vmx, snap_name=None, log_level=1, interac print "[!] Error while trying to find VBoxManage.exe. Try again without -I." sys.exit(1) - - # Grab vmx, snapshot name and log level if we're in interactive mode - if interactive: vmx = raw_input("[*] Please enter the VirtualBox virtual machine name: ") snap_name = raw_input("[*] Please enter the snapshot name: ") log_level = raw_input("[*] Please enter the log level (default 1): ") - if log_level: log_level = int(log_level) else: log_level = 1 - # if we're on windows, get the DOS path names - if os.name == "nt": self.vmrun = GetShortPathName(r"%s" % vmrun) self.vmx = GetShortPathName(r"%s" % vmx) else: - self.vmrun = vmrun self.vmx = vmx @@ -406,12 +384,7 @@ def __init__ (self, host, port, vmrun, vmx, snap_name=None, log_level=1, interac self.log("\t log level: %d" % self.log_level) self.log("Awaiting requests...") - ### - ### VBOXMANAGE COMMAND WRAPPERS - ### - - - def delete_snapshot (self, snap_name=None): + def delete_snapshot(self, snap_name=None): if not snap_name: snap_name = self.snap_name @@ -420,40 +393,37 @@ def delete_snapshot (self, snap_name=None): command = self.vmrun + " snapshot " + self.vmx + " delete " + snap_name + '"' return self.vmcommand(command) - - def list (self): + def list(self): self.log("listing running images", 2) command = self.vmrun + " list runningvms" return self.vmcommand(command) - - def list_snapshots (self): + def list_snapshots(self): self.log("listing snapshots", 2) command = self.vmrun + " snapshot " + self.vmx + " list" return self.vmcommand(command) - - def reset (self): + def reset(self): self.log("resetting image", 2) command = self.vmrun + " controlvm " + self.vmx + " reset" return self.vmcommand(command) - def pause (self): + def pause(self): self.log("pausing image", 2) command = self.vmrun + " controlvm " + self.vmx + " pause" return self.vmcommand(command) - def resume (self): + def resume(self): self.log("resuming image", 2) command = self.vmrun + " controlvm " + self.vmx + " resume" return self.vmcommand(command) - def revert_to_snapshot (self, snap_name=None): + def revert_to_snapshot(self, snap_name=None): if not snap_name: snap_name = self.snap_name @@ -461,22 +431,18 @@ def revert_to_snapshot (self, snap_name=None): if self.is_target_running(): self.stop() - - self.log("reverting to snapshot: %s" % snap_name, 2) command = self.vmrun + " snapshot " + self.vmx + " restore " + snap_name return self.vmcommand(command) - - def snapshot (self, snap_name=None): + def snapshot(self, snap_name=None): if not snap_name: snap_name = self.snap_name #VirtualBox flips out if you try to do this with a running VM if self.is_target_running(): - self.pause() - + self.pause() self.log("taking snapshot: %s" % snap_name, 2) @@ -484,43 +450,33 @@ def snapshot (self, snap_name=None): return self.vmcommand(command) - - def start (self): + def start(self): self.log("starting image", 2) command = self.vmrun + " startvm " + self.vmx # TODO: we may want to do more here with headless, gui, etc... return self.vmcommand(command) - - def stop (self): + def stop(self): self.log("stopping image", 2) command = self.vmrun + " controlvm " + self.vmx + " poweroff" return self.vmcommand(command) - - def suspend (self): + def suspend(self): self.log("suspending image", 2) command = self.vmrun + " controlvm " + self.vmx + " pause" return self.vmcommand(command) - - ### - ### EXTENDED COMMANDS - ### - -#added a function here to get vminfo... useful for parsing stuff out later + #added a function here to get vminfo... useful for parsing stuff out later def get_vminfo(self): + self.log("getting vminfo", 2) - self.log("getting vminfo", 2) - - command = self.vmrun + " showvminfo " + self.vmx + " --machinereadable" - return self.vmcommand(command) - + command = self.vmrun + " showvminfo " + self.vmx + " --machinereadable" + return self.vmcommand(command) - def restart_target (self): + def restart_target(self): self.log("restarting virtual machine...") #VirtualBox flips out if you try to do this with a running VM @@ -534,8 +490,7 @@ def restart_target (self): # wait for the snapshot to come alive. self.wait() - - def is_target_running (self): + def is_target_running(self): # sometimes vmrun reports that the VM is up while it's still reverting. time.sleep(10) @@ -545,7 +500,7 @@ def is_target_running (self): return False - def is_target_paused (self): + def is_target_paused(self): time.sleep(10) for line in self.get_vminfo().split('\n'): @@ -554,43 +509,77 @@ def is_target_paused (self): return False +if __name__ == "__main__": + opts = None -######################################################################################################################## + vmrun_arg = r"C:\progra~1\vmware\vmware~1\vmrun.exe" + vmx_arg = None + snap_name_arg = None + log_level_arg = 1 + interactive_arg = False + virtualbox_arg = False + port_arg = None -if __name__ == "__main__": # parse command line options. try: - opts, args = getopt.getopt(sys.argv[1:], "x:r:s:l:i", ["vmx=", "vmrun=", "snapshot=", "log_level=", "interactive", "port=", "vbox"]) + opts, args = getopt.getopt( + sys.argv[1:], + "x:r:s:l:i", [ + "vmx=", + "vmrun=", + "snapshot=", + "log_level=", + "interactive", + "port=", + "vbox" + ] + ) except getopt.GetoptError: ERR(USAGE) - - vmrun = r"C:\progra~1\vmware\vmware~1\vmrun.exe" - vmx = None - snap_name = None - log_level = 1 - interactive = False - virtualbox = False for opt, arg in opts: - if opt in ("-x", "--vmx"): vmx = arg - if opt in ("-r", "--vmrun"): vmrun = arg - if opt in ("-s", "--snapshot"): snap_name = arg - if opt in ("-l", "--log_level"): log_level = int(arg) - if opt in ("-i", "--interactive"): interactive = True - if opt in ("--port"): PORT = int(arg) - if opt in ("--vbox"): virtualbox = True + if opt in ("-x", "--vmx"): + vmx_arg = arg + if opt in ("-r", "--vmrun"): + vmrun_arg = arg + if opt in ("-s", "--snapshot"): + snap_name_arg = arg + if opt in ("-l", "--log_level"): + log_level_arg = int(arg) + if opt in ("-i", "--interactive"): + interactive_arg = True + if opt in ("-p", "--port"): + port_arg = int(arg) + if opt in ("-v", "--vbox"): + virtualbox_arg = True # OS check - if interactive and not os.name == "nt": + if interactive_arg and not os.name == "nt": print "[!] Interactive mode currently only works on Windows operating systems." ERR(USAGE) - if not vmx and not interactive: + if not vmx_arg and not interactive_arg: ERR(USAGE) - if not virtualbox: - servlet = vmcontrol_pedrpc_server("0.0.0.0", PORT, vmrun, vmx, snap_name, log_level, interactive) - elif virtualbox: - servlet = vboxcontrol_pedrpc_server("0.0.0.0", PORT, vmrun, vmx, snap_name, log_level, interactive) + if not virtualbox_arg: + servlet = VMControlPedrpcServer( + "0.0.0.0", + port_arg, + vmrun_arg, + vmx_arg, + snap_name_arg, + log_level_arg, + interactive_arg + ) + else: + servlet = VBoxControlPedrpcServer( + "0.0.0.0", + port_arg, + vmrun_arg, + vmx_arg, + snap_name_arg, + log_level_arg, + interactive_arg + ) servlet.serve_forever() diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/app.py b/web/app.py new file mode 100644 index 0000000..0ef6229 --- /dev/null +++ b/web/app.py @@ -0,0 +1,85 @@ +from flask import Flask, render_template, redirect +import re + +app = Flask(__name__) +app.session = None + + +def commify(number): + number = str(number) + processing = 1 + regex = re.compile(r"^(-?\d+)(\d{3})") + while processing: + (number, processing) = regex.subn(r"\1,\2", number) + return number + + +@app.route("/togglepause") +def pause(): + # Flip our state + app.session.pause_flag = not app.session.pause_flag + return redirect('/') + + +@app.route('/view_crash/') +def view_crash(crash_id): + return render_template("view_crash.html", crashinfo=app.session.procmon_results[crash_id]) + + +@app.route("/") +def index(): + crashes = [] + procmon_result_keys = app.session.procmon_results.keys() + procmon_result_keys.sort() + + for key in procmon_result_keys: + val = app.session.procmon_results[key] + status_bytes = " " + + if key in app.session.netmon_results: + status_bytes = commify(app.session.netmon_results[key]) + + crash = { + "key": key, + "value": val.split("\n")[0], + "status_bytes": status_bytes + } + crashes.append(crash) + + # which node (request) are we currently fuzzing. + if app.session.fuzz_node.name: + current_name = app.session.fuzz_node.name + else: + current_name = "[N/A]" + + # render sweet progress bars. + mutant_index = float(app.session.fuzz_node.mutant_index) + num_mutations = float(app.session.fuzz_node.num_mutations()) + + progress_current = mutant_index / num_mutations + num_bars = int(progress_current * 50) + progress_current_bar = "[" + "=" * num_bars + " " * (50 - num_bars) + "]" + progress_current = "%.3f%%" % (progress_current * 100) + + total_mutant_index = float(app.session.total_mutant_index) + total_num_mutations = float(app.session.total_num_mutations) + + progress_total = total_mutant_index / total_num_mutations + num_bars = int(progress_total * 50) + progress_total_bar = "[" + "=" * num_bars + " " * (50 - num_bars) + "]" + progress_total = "%.3f%%" % (progress_total * 100) + + state = { + "session": app.session, + "current_mutant_index": commify(app.session.fuzz_node.mutant_index), + "current_name": current_name, + "current_num_mutations": commify(app.session.fuzz_node.num_mutations()), + "progress_current": progress_current, + "progress_current_bar": progress_current_bar, + "progress_total": progress_total, + "progress_total_bar": progress_total_bar, + "total_mutant_index": commify(app.session.total_mutant_index), + "total_num_mutations": commify(app.session.total_num_mutations), + } + + return render_template('index.html', state=state, crashes=crashes) \ No newline at end of file diff --git a/web/static/css/sulley.css b/web/static/css/sulley.css new file mode 100644 index 0000000..67afed5 --- /dev/null +++ b/web/static/css/sulley.css @@ -0,0 +1,46 @@ +a:link {color: #FF8200; text-decoration: none;} +a:visited {color: #FF8200; text-decoration: none;} +a:hover {color: #C5C5C5; text-decoration: none;} + +body { + background-color: #000000; + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + color: #FFFFFF; +} + +td { + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + color: #A0B0B0; +} + +.fixed { + font-family: Courier New, sans-serif; + font-size: 12px; + color: #A0B0B0; +} + +.input { + font-family: Arial, Helvetica, sans-serif; + font-size: 11px; + color: #FFFFFF; + background-color: #333333; + border: thin none; + height: 20px; +} + +.running { + color: green; +} + +.paused { + color: red; +} + +.main-wrapper { + margin-left: auto; + margin-right: auto; + display: block; + width: 750px; +} \ No newline at end of file diff --git a/web/templates/base.html b/web/templates/base.html new file mode 100644 index 0000000..f38c793 --- /dev/null +++ b/web/templates/base.html @@ -0,0 +1,12 @@ + + + + + + Sulley Fuzz Control + + + + {% block body %} {% endblock %} + + \ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..6df6f3f --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,104 @@ +{% extends "base.html" %} +{% block body %} + +
+ + + + +
+ + + + + + + + + + + +
+
Sulley Fuzz Control
+
+ {% if state.session.is_paused %} +
PAUSED
+ {% else %} +
RUNNING
+ {% endif %} +
+ + + + + + + + + + + + + + + + + +
+ Total: + + {{ state.total_mutant_index }} + + of + + {{ state.total_num_mutations }} + + {{ state.progress_total_bar | safe }} + + {{ state.progress_total }} +
+ + {{ state.current_name }}: + + + {{ state.current_mutant_index }} + + of + + {{ state.current_num_mutations }} + + {{ state.progress_current_bar | safe }} + + {{ state.progress_current }} +
+
+
+ +
+
+ + + + + + + + {% for crash in crashes %} + + + + + + {% endfor %} +
Test Case #Crash SynopsisCaptured Bytes
+ + {{crash.key}} + + + {{crash.value}} + + {{crash.status_bytes}} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/web/templates/view_crash.html b/web/templates/view_crash.html new file mode 100644 index 0000000..2432342 --- /dev/null +++ b/web/templates/view_crash.html @@ -0,0 +1,25 @@ +{% extends base.html %} +{% block body %} +
+ + + + +
+ + + + + + + +
+
Crash Viewer
+
+
+                {{ crashinfo }}
+              
+
+
+
+{% endblock %} \ No newline at end of file