Let's look at the basic API calls to the Spike framework. While the Spike framework contains many more APIs, these basic blocks are often needed to write a fuzzer. A small description is also included to facilitate the learning curve of writing a fuzzer with Spike.
The Spike API generally expects a nomenclature that respects these general rules:
The following code is a reference table of the different functions provided by Spike and an explanation of their meaning and use. Those functions are really useful for writing different parts of the fuzzer such as block, block size, string, and binaries values, and for managing basic IP communication.
s_add_fuzzstring(char * fuzzstring); //Adding fuzzstring to the framework int s_string(char * instring); //A normal ascii string int s_binary(char * instring); // Binary input to push on the spike pile int s_bigword(unsigned int aword); int s_binary_repeat(char *instring, int times); //Repeat the binary X times int s_string_repeat(char *instring, int times); //Repeat the string X times void intel_order(char * buffer, int length); // size in intel order int s_intelword(unsigned int aword); //push a word in intel order int s_intelhalfword(unsigned short ashort); // push a half word in intel order void s_string_variables(unsigned char splitchar, unsigned char *variables); void s_string_variable(unsigned char *variable); //String to be fuzzed /** Block based API **/ int s_block_start(char *blockname); //Starting a block int s_block_end(char * blockname); // Ending a block /** Binary Block Size API **/ // Put the size of the block in a binary format // post-fix meaning of those api // intel = value in Intel/Small Endian order // bigendian= Value in Big Endian order // byte = Value is represented on one byte // halfword = Value is represented on a halfword // word = Value is represented on a word // mult = the value is multiple by X // plus = X is added to the size value // variable = This value will be fuzzed during the tests. int s_binary_block_size_intel_word(char *blockname); int s_binary_block_size_intel_word_plus(char *blockname,long some); int s_binary_block_size_word_halfword_bigendian_variable(char *blockname); int s_binary_block_size_word_bigendian_variable(char *blockname); int s_binary_block_size_byte_variable(char * blockname); int s_binary_block_size_intel_halfword_variable(char *blockname); int s_binary_block_size_intel_word_variable(char *blockname); int s_binary_block_size_intel_halfword_plus_variable(char *blockname,long plus); int s_binary_block_size_halfword_bigendian_mult(char * blockname, float mult); int s_binary_block_size_word_bigendian(char *blockname); int s_binary_block_size_intel_halfword(char *blockname); int s_binary_block_size_halfword_bigendian(char * blockname); int s_binary_block_size_intel_halfword_plus(char *blockname,long plus); int s_binary_block_size_byte(char * blockname); int s_binary_block_size_byte_plus(char * blockname, long plus); int s_binary_block_size_word_bigendian_plussome(char *blockname,long some); int s_binary_block_size_byte_mult(char * blockname, float mult); int s_binary_block_size_word_bigendian_mult(char *blockname, float mult); int s_binary_block_size_word_intel_mult_plus(char *blockname, long some, float mult); int s_binary_block_size_intel_halfword_mult(char *blockname,float mult); int s_blocksize_string(char * instring, int size); // Size of the block in a string int s_blocksize_unsigned_string_variable(char * instring, int size);// unsigned size and fuzzed int s_blocksize_asciihex(char * blockname); //block size in ascii hex. Format int s_blocksize_asciihex_variable(char * blockname); //block size in ascii hex. and fuzzed /** UPD protocol API **/ int spike_send_udp(char * host, int port); // connect to a UDP port and send spike int spike_connect_udp(char * host, int port); // connect to a UDP port int spike_listen_udp(int port); // listen on UDP port void s_close_udp( ); //Close a udp connection /** TCP protocol API **/ int spike_connect_tcp(char * host, int port); //Open a tcp connection int spike_send_tcp(char * host, int port); //Open connection and push the spike int spike_send( ); //push the current spike to the open connection int s_send_lines( ); //send one line at a time int s_tcp_accept(int listenfd); //Accept incoming connection void spike_close_tcp( ); //Close the tcp socket void s_read_packet( ); //Read packet with some printing int s_readline( ); // Read from the open connection int s_read_bigendian_word_packet(char **buf); //Read a packet delimitated in a bigendian word size
Sooner or later you will have to write your own fuzzer to test a specific bug or to cover a new protocol. When that occurs, the first step is to look to see whether there is an RFC or a decoder (for a sniffer like Wireshark, see Ethereal/Wireshark) so you can gather the appropriate information to structure the fuzzer. These applications are often a gold mine of information because they contain the necessary details for structuring the fuzzer without much overhead and they eliminate the task of reversing a binary protocol.
If the RFC or decoder is not available, you'll have to get your hands dirty using bits and bytes. Let's review the basic steps of reversing a simple protocol:
Place the packet load in a Spike template using s_binary
block and using the appropriate s_send_tcp
and s_read_tcp
to be sure that your basic communication can be reproduced. Use a sniffer to verify that the same request gives back the same results. Using requests with the same results every time simplifies the process of reversing, and it allows you to add checks to ensure the fuzzer did not get corrupted during development:
s_binary ("00 00 00 56 ff 53 4d 42 25 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 08 14 0f 00 08 00 00 11 00 00 01 00 00 04 e0 ff 00 00 00 00 00 00 00 00 00 00 00 00 55 00 01 00 55 00 03 00 01 00 00 00 00 00 11 00 5c 4d 41 49 4c 53 4c 4f 54 5c 4c 414e 4d 41 4e");
Replace the string by s_string
. I like to do this because it adds a little bit more clarity to the payload, while not changing its behavior:
s_binary ("00 00 00 56 ff"); s_string("SMB"); # ASCII string s_binary ("25 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 08 14 0f 00 08 00 00 11 00 00 01 00 00 04 e0 ff 00 00 00 00 00 00 00 00 00 00 00 00 55 00 01 00 55 00 03 00 01 00 00 00 00 00 11 00"); s_string("\MAILSLOT\LANMAN"); # ASCII string
Look for the size of the load. You need to find the load's size to be sure that the packet stays conformed and is not discarded as soon as you start modifying other items. As mentioned previously, the size value is usually within the header. There are usually two kinds of sizes: the total packet size (which is easy to find since you are looking at the payload [known value]
length written in big/small endian in a half-word or a word) or the size of a subpart of the protocol (e.g., sometimes the size of the next structure). For example:
s_binary("00 00"); s_binary_block_size_halfword_bigendian(payload1); # Size of payload1 s_block_start(payload1); # begin of payload1 s_binary("ff"); s_string("SMB"); s_binary("25 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 08 14 0f 00 08 00 00 11 00 00 01 00 00 04 e0 ff 00 00 00 00 00 00 00 00 00 00 00 00 55 00 01 00 55 00 03 00 01 00 00 00 00 00 11 00"); s_string("\MAILSLOT\LANMAN"); s_block_end(payload1); # end of payload1
Sometimes strings have their own size value visible in front of themselves. Sometimes there may be a size value that references the rest of the packet. It is very important to find all those size bytes—otherwise, the data is just treated as junk. Sometimes it is obvious; other times it is not. Give special attention to strings that are not null-terminated (the protocol needs to know where they end):
s_binary("00 00"); s_binary_block_size_halfword_bigendian(payload1); s_block_start(payload1); s_binary("ff"); s_string("SMB"); s_binary ("25 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 08 14 0f 00 08 00 00 11 00 00 01 00 00 04 e0 ff 00 00 00 00 00 00 00 00 00 00 00 00 55 00 01 00 55 00 03 00 01 00 00 00 00 00");s_binary_block_size_intel_halfword(payload2); #size of payload2
s_block_start(payload2); #begin payload2
s_string("\MAILSLOT\LANMAN"); s_block_end(payload2);  # end payload2 s_block_end(payload1);
Another problem that occurs infrequently (but is worth watching out for) is a protocol that does not rely on the TCP/IP checksum system and instead implements its own. If these values are not updated adequately, the data is discarded and you're back to square one.
When you start fuzzing, be sure to verify that your basic communication still works correctly by placing one variable that can be fuzzed. At the same time, use a network analyzer such as Wireshark to ensure that proper communication still occurs and that the packet is not just discarded. If the latter happens, it usually means that a size value or a check was wrongly identified; you must review your fuzzing script before going further. In this case, receiving an error that the path was not found is a sign that the packet reached a good processing point that we want to fuzz:
s_binary("00 00");
s_binary_block_size_halfword_bigendian(payload1);
s_block_start(payload1);
s_binairy("ff");
s_string("SMB");
s_binary ("25 00
00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00
00 00 00 08 14 0f 00 08 00 00 11 00 00 01 00 00
04 e0 ff 00 00 00 00 00 00 00 00 00 00 00 00 55
00 01 00 55 00 03 00 01 00 00 00 00 00");
s_binary_block_size_intel_halfword(payload2);
s_block_start(payload2);
s_string("\MAILSLOT\");
s_string_variable("LANMAN");  # The first variable to be fuzz.
s_block_end(payload2);
s_block_end(payload1);
A smart way to improve your fuzzer is to test it against already known vulnerabilities. Be sure that it triggers all the vulnerabilities for a given protocol because usually vulnerabilities in one application have a counterpart in other applications (after all, developers read the same RFC specification and development manuals). When developing your fuzzer, you will find that similar mistakes happen at multiple places.