Bug Hunting Using Fuzzing and Static Analysis

Learn advanced bug hunting techniques including fuzzing and static analysis to identify software vulnerabilities. This lab covers manual code auditing, fuzzing with Spike, Metasploit FTP fuzzing, and CTF challenges.

Lab Overview

Identifying and fixing software vulnerabilities is of paramount importance. This lab introduces two techniques for bug hunting: Fuzzing and Static Analysis. These methods are essential for uncovering hidden security flaws in software, which can be exploited for malicious purposes if left unaddressed. Fuzzing involves sending unexpected and often malformed data as input to a program, searching for weaknesses, while Static Analysis employs automated tools to analyze the code structure for potential issues. This lab provides a hands-on experience in finding and exploiting vulnerabilities.

In this lab, you will learn how to manually audit C code to spot errors and use various fuzzing techniques to test network programs for security flaws. You will start by auditing and securing C code, identifying vulnerabilities, and fixing potential issues. You will then explore the world of fuzzing, where you will learn to use tools like Spike to send various inputs to network services to uncover potential vulnerabilities. The lab also guides you through Metasploit’s FTP fuzzing module. Finally, you will apply your knowledge to CTF challenges, running and fuzzing network services to crash programs and uncover flags. By the end of this lab, you will have gained practical skills and knowledge in software security testing and vulnerability detection.


Contents

    Introduction to bug hunting

    There are many kinds of programming and design mistakes that can lead to serious security flaws. Auditing software for flaws is an important component of secure software development and testing. After software has been created (and during the development process), programs can be tested with or without access to the source code to look for vulnerabilities. If a security researcher can find a flaw that the software authors do not know about, this can have serious security ramifications. If they then weaponise the attack, and create an exploit to attack, this is known as a zero-day exploit, if there is currently no fix available to defend against the attack.

    Zero-day exploits can be used for malicious purposes, such as building worms or in targeted attacks, and can be sold on the blackmarket. White hat hackers (the good guys, like you!) will generally follow responsible disclosure, and notify the vendor and give them time to fix the problem before going public with the information, after a fair amount of time. Many of the larger vendors offer rewards for reporting vulnerabilities.

    Warning: Please follow responsible disclosure, when you discover zero-days. This will give you warm-fuzzy feelings, build your reputation within the security industry, and you may even make yourself some money in the process.

    When you have access to the source code you can perform:

    • manual code review, to look for security mistakes
    • static analysis, which is when you use software to automatically analyse the code

    If you do not have access to the source code, you can use:

    • reverse engineering to transform the the compiled code (program) to get a (harder to read) version of the source code — binary static analysis is possible analysing a program without the original source code
    • fuzzing to do blackbox testing to find faults by feeding in variations of unexpected input into a program in an attempt to uncover unexpected behaviour

    Even with access to source code, fuzz testing provides an insight into the behaviour of the program under unexpected inputs.

    Manual code review

    Being able to spot errors in code is an important skill for developers and security professionals.

    Spot the error:

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #define MAXPASS 7
    int main()
    {
        char correct_pass[7] = "SecreT\0";
        char input_pass[7];
        do{
            printf("Enter password:\n");
            scanf("%s", &input_pass);
            // Uncomment the next line to see what is happening...
            // printf("You entered %s, pass is %s\n", input_pass, correct_pass);
            if(strcmp(input_pass, correct_pass) == 0) {
                printf("Access granted\n");
            } else {
                printf("Access denied\n");
            }
        } while(strcmp(input_pass, correct_pass) != 0);
    }
    

    Question: Can you spot the error, without compiling and running the code?

    ==VM: On Desktop Debian Linux VM==

    ==action: Save, compile, debug, and test this program==.

    ==action: Try uncommenting the printf line, and see what happens when various inputs are used==.

    Question: Is this program secure? Why not?

    ==action: Run and manually exploit this code==.

    Tip: If you cannot find a way of exploiting the vulnerability, it is possible that the compiler you are using is generating instructions that allocate memory addresses in a different order. Try swapping the two variable declaration lines (as below), recompile, and run the new version.

    char correct_pass[7] = "SecreT\0";
    char input_pass[7];
    

    Action: Update the above code to prevent the attack.

    ==action: Spot the error:==

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #define MAXPASS 7
    int main()
    {
        char *correct_pass, *input_pass;
        correct_pass = malloc(sizeof(char)*MAXPASS);
        input_pass = malloc(sizeof(char)*MAXPASS);
        strcpy(correct_pass, "SecreT\0");
    
        printf("Please enter the password: ");
        scanf("%7s", input_pass);
        // uncomment to see what is happening...
        // printf("You entered %s, pass is %s\n", input_pass, correct_pass);
        if(strcmp(input_pass, correct_pass) == 0) {
            printf("Access granted\n");
        } else {
            printf("Access denied\n");
        }
    }
    

    Question: Can you spot the error, without compiling and running the code?

    Hint: Consider defensive programming and memory allocation. Note that it might not be practical to exploit this.

    Warning: SPOILER ALERT!

    Note: This is more of a potential bug, rather than a glaring exploitable vulnerability. The code does not check that malloc successfully allocated memory, if you then read or write to memory that failed to be allocated, it will cause the program to behave incorrectly.

    ==action: Update the code to prevent the problem==.

    Fuzzing

    Fuzzing involves sending deliberately unexpected data as input to a program in an attempt at discovering programming flaws. Fuzzers are often used by security researchers to find new vulnerabilities in software.

    Three popular network fuzzers are Spike, Sulley, and Peach. If you are testing Web apps or GUI programs, other specialised fuzzers exist, such as the Web app fuzzers in Burp Suite and OWASP Zap.

    Spike is a popular fuzzer written in C. It works by reading in a scripted template file describing the normal protocol (what is normally expected as input), and it automatically feeds in data based on its collection of strings that are likely to crash the program by deviating from what is expected. For example, it may try invalid characters/symbols, and longer than expected inputs.

    Tip: Although popular, Spike’s lack of documentation can be frustrating.

    ==VM: On your Desktop Debian Linux VM==

    ==action: Run Ncat as a network service on port 4444==:

    ncat -vlk -p 4444
    

    ==VM: On your Kali Linux VM==

    ==action: Start Wireshark, and monitor the traffic to view the stream of traffic==:

    sudo wireshark
    

    Next, let’s see Spike in action…

    ==action: Open a new terminal tab (Shift+Ctrl+T)==.

    ==action: Create and edit a file named “my_first_spike.spk”. Enter this text==:

    s_string("Hello, world!");
    

    This spike script simply sends the greeting string to the server.

    To summarise:

    • s_string() sends a string

    Spike has a number of interpreters for interacting with programs.

    Since we are using Ncat to simulate a network service, we can use one of Spike’s most common interpreters, generic_send_tcp.

    The usage for generic_send_tcp is:

    Usage: ./generic_send_tcp host port spike_script SKIPVAR SKIPSTR
    

    ==action: Run our spike script==:

    generic_send_tcp ==edit:DESKTOP_IP_ADDRESS== 4444 my_first_spike.spk 0 0
    

    ==action: View the Ncat output on the Desktop VM to confirm that Spike sends out the expected text==.

    ==VM: On your Desktop Debian Linux VM==

    ==action: Use Wireshark, and follow the TCP stream, to view the behaviour of the fuzzer==.

    ==VM: On your Kali Linux VM==

    Note that it tries a number of different network connection attempts, sending this same payload data (with different TCP options).

    At this point the fuzzer is not really doing anything interesting, as we have not specified any values to fuzz.

    ==action: Edit “my_first_spike.spk”. Change the line to==:

    s_string_variable("Hello, world!");
    

    ==action: Try running this new version of the script==:

    generic_send_tcp ==edit:DESKTOP_IP_ADDRESS== 4444 my_first_spike.spk 0 0
    

    ==action: Watch the Ncat output in the previous tab==.

    As we can see the spike script now involves some visible “fuzziness”. The first time the text “Hello, world!” is sent, and subsequently other malformed versions are sent.

    In this case we are not going to find anything that breaks our Ncat server, so let’s move on to fuzzing some vulnerable software.

    ==VM: On your Desktop Debian Linux VM==

    ==action: Create and edit a new file “another_vulnerable_service.c”==:

    vi another_vulnerable_service.c
    

    ==action: Enter the below C code==:

    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netdb.h>
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    
    int main()
    {
      // variables
      int sock_fd, connection_fd;
      const int buffer2_len = 50;
      const int buffer1_len = 1000;
      char buffer2[buffer2_len];
      char buffer1[buffer1_len];
      struct sockaddr_in addr;
    
      // socket details
      addr.sin_family = AF_INET;
      addr.sin_addr.s_addr = htons(INADDR_ANY);
      addr.sin_port = htons(5555);
    
      // create socket
      sock_fd = socket(AF_INET, SOCK_STREAM, 0);
       
      // set socket details
      bind(sock_fd, (struct sockaddr *) &addr, sizeof(addr));
       
      // start listening for connections
      listen(sock_fd, 10);
      // keep listening for new connections
      while(1) {
        // accept connection
        connection_fd = accept(sock_fd, NULL, NULL);
        // get name
        bzero(buffer1, buffer1_len);
        sprintf(buffer1, "What is your name?\r\n");
        write(connection_fd, buffer1, strlen(buffer1)+1);
          
        bzero(buffer1, buffer1_len);
        read(connection_fd, buffer1, buffer1_len);
        printf("Connection from %s\n", buffer1);
        // reply
        bzero(buffer2, buffer2_len);
        snprintf(buffer2, buffer2_len, "Hello, %s\r\n", buffer1);
        write(connection_fd, buffer2, strlen(buffer2)+1);
        // if name starts with Cliffe, ask for another string, and print back
        if(strncmp(buffer1, "Cliffe", 6) == 0 ||strncmp(buffer1, "Tom", 3) == 0) {
          printf("(Access granted)\n");
          bzero(buffer1, buffer1_len);
          sprintf(buffer1, "Access granted... Please enter a string:\n");
          write(connection_fd, buffer1, strlen(buffer1)+1);
    
          bzero(buffer1, buffer1_len);
          read(connection_fd, buffer1, buffer1_len);
          printf("Received string: %s\n", buffer1);
    
          strcpy(buffer2, buffer1);
          sprintf(buffer1, "You entered: %s\r\n", buffer2);
          write(connection_fd, buffer1, strlen(buffer1)+1);
        }
    
        close(connection_fd);
      }
    }
    

    ==action: Compile this program to “another_vulnerable_service”==:

    gcc -g -m32 -fno-stack-protector -z norelro another_vulnerable_service.c -o another_vulnerable_service
    

    ==action: Start our vulnerable service (listening on port 5555)==.

    ./another_vulnerable_service
    

    ==VM: On your Kali Linux VM==

    Connecting to your vulnerable service:

    ==action: Open a new terminal tab (Ctrl-Shift-T)==.

    nc -v ==edit:DESKTOP_IP_ADDRESS== 5555
    

    ==action: Type some text and press Enter, then some more text and Enter again==. This sends the text to the server, which responds with the output from your program.

    ==action: Identify the order that messages are send to and received from the vulnerable service==.

    ==action: Create and edit a file named “my_name_spike.spk”. Enter this text==:

    printf("Fuzzing...\n");
    s_readline();
    s_string_variable("==edit:Bob==");
    s_string("\r\n");
    spike_send();
    
    s_readline();
    
    s_string_variable("==edit:A string!==");
    s_string("\r\n");
    s_readline();
    spike_send();
    

    Note that:

    • printf() writes to the local console
    • s_readline() reads and displays a line of response
    • s_string_variable() writes a string that is fuzzed
    • spike_send() sends what is in the buffer now
    • The best way to know what commands are available is to look at the source code of spike.h (if you like, you can view the source code here)

    ==action: Try running the spike script to test the vulnerable program==:

    generic_send_tcp ==edit:DESKTOP_IP_ADDRESS== 5555 my_name_spike.spk 0 0
    

    The script will try to run through a large list of fuzzed inputs, in an attempt to crash the program.

    If the fuzzer stops while the program is still running, simply restart the fuzzer on the next variable. For example, if the fuzzer stops on:

    Fuzzing Variable 0:==edit:661==
    Fuzzing...
    line read=What is your name?
    Variablesize= 0
    returning end of line\!
    

    ==action: Press Ctrl-C and continue on variable 0 iteration 662==:

    generic_send_tcp ==edit:DESKTOP_IP_ADDRESS== 5555 my_name_spike.spk 0 ==edit:662==
    

    You may find that the fuzzer does not find a way of crashing the program.

    ==action: Edit the spike script, so that the fuzzer gets past the first condition within the vulnerable service code==.

    Hint: edit the spike script so it sends a name that passes the string comparison.

    ==action: Try running the spike script again==:

    generic_send_tcp ==edit:DESKTOP_IP_ADDRESS== 5555 my_name_spike.spk 0 0
    

    If you have created an effective spike, the program will eventually crash after some fuzzing. When the program crashes, the message “tried to send to a closed socket!” will appear in the terminal tab where Spike is running. Press Ctrl+C to stop fuzzing, and the number of attempts will be shown. This can be used to determine the string that was used to crash the program.

    ==action: View the traffic in Wireshark, and determine the last input that the fuzzer sent==.

    ==VM: On your Desktop Debian Linux VM==

    ==action: Restart the service (in the previous tab)==:

    ./another_vulnerable_service
    

    ==VM: On your Kali Linux VM==

    ==action: Connect manually with Ncat==:

    nc -v ==edit:DESKTOP_IP_ADDRESS== 5555
    

    ==action: Enter the input (possibly multiple lines) that the fuzzer used to crash the program==.

    ==action: Make sure you can reproduce the crash==.

    ==action: Save the crash inducing input (multiple lines) into a text file named “fuzzed”==.

    ==VM: On your Desktop Debian Linux VM==

    ==action: Run the vulnerable server in a debugger==:

    gdb ./another_vulnerable_service
    
    (gdb) run
    

    ==VM: On your Kali Linux VM==

    ==action: Feed in the fuzzed input==:

    nc -v ==edit:DESKTOP_IP_ADDRESS== 5555 < fuzzed
    

    ==VM: On your Desktop Debian Linux VM==

    ==action: Back in the GDB terminal, look at the details in the debugger==:

    (gdb) backtrace
    

    ==action: Display the value stored in registers==:

    (gdb) layout split
    
    (gdb) layout regs
    

    Question: Explain any interesting values and their significance. ==hint:Hint: look for the ASCII values of the input==.

    ==action: Resume the fuzzing at the point that it stopped for the crash==, by changing the last two arguments (previously you used “0 0”). The first number represents the fuzzing variable (such as string_variable) to skip to, and the second number is the fuzzing iteration to skip to for that variable (for example, “AAAAAAA”).

    In order to fuzz more complex programs, the spike script file needs to have added complexity to send certain parts of the input as expected, and focus on fuzzing the inputs that are most likely to trigger flaws:

    • Altering input lengths and command options
    • Altering integers, to test for boundary conditions and outliers
    • Command injections, invalid characters and so on

    Fuzzers are very good at finding shallow bugs. However, it can be quite hard for a fuzzer to find deep bugs (behind a number of conditional code statements), since the fuzzer needs to try to get good code coverage, to test the behaviour of all the possible control flows. However, complete coverage can be impractical or impossible for complex programs.

    One of the most effective ways to fuzz a network program is to use a sniffer to record examples of normal traffic (and/or study protocol RFC specifications), and then create a fuzzer that follows the normal expected flow of communication, with fuzzed input for the situations that are most likely to be vulnerable.

    Action: Fix the security error(s) in another_vulnerable_service.

    Tip: Also, consider fixing the error handling: for example, gracefully exit with an error message if the port is already in use. And if you want more practice writing C (and with multithreading), update the program to accept multiple connections at once (search for example code online).

    Fuzzing FTP servers

    ==VM: On your Windows VM==

    ==action: Log in as your randomised username with password “tiaspbiqe2r”==.

    The programs you will be fuzzing first are vulnerable (and real) FTP servers “FreeFloat”, and “EasyFTP”.

    ==action: Open file explorer, and browse to “C:\Users\vagrant\Downloads\freefloatftpserver\”==

    One of these FTP servers may already be running. You may want to check by connecting manually via ncat.

    Tip: Note that each time the fuzzer crashes the program you will need to make note of the values that crashed the server then restart it.

    ==action: Make sure FreeFloat FTP server is running==.

    ==VM: On your Kali Linux VM==

    ==action: Run an FTP spike script: (where Win_IP_address it the IP of your Windows VM)==

    generic_send_tcp ==edit:Win_IP_address== 21 /usr/share/spike/audits/FTPD/ftpd1.spk 0 0
    

    Note: Again, if the fuzzer stops without crashing the FTP server program try restarting at the same point, or over from the beginning.

    Question: Does it crash the program? What input crashed the program?

    Hint: Using Wireshark may be the easiest way to identify the input.

    ==action: Have a look through the spike script file, and try to understand how it works==.

    Question: Does it crash the program? What input crashed the program?

    Metasploit has an FTP fuzzing module, try using it:

    ==action: Restart the FTP server on your Windows VM, if it has crashed==.

    msfconsole
    
    msf > use auxiliary/fuzzers/ftp/ftp_pre_post
    
    msf auxiliary(ftp_pre_post) > set RHOSTS ==edit:Win_IP_address==
    
    msf auxiliary(ftp_pre_post) > exploit
    

    Question: Does it crash the program? What input crashed the program?

    ==action: Have a look at the Metasploit module, and try to understand how it works==.

    less /usr/share/metasploit-framework/modules/auxiliary/fuzzers/ftp/ftp_pre_post.rb
    

    or view the code online

    ==action: Repeat the above steps to fuzz the EasyFTP server.==

    EasyFTP can be found in C:\Users\vagrant\Downloads\easyftp

    ==action: Make a note of your findings==.

    Fuzzing CTF challenges

    ==VM: On your Desktop Debian Linux VM==

    ==action: Browse the challenges directory==

    ls ~/challenges
    

    When you run each program it will give you instructions and hints on how to solve the challenge.

    Action: Run each of the challenges (always after changing to the directory first):

    cd ~/challenges/Ch_Fuzz_1
    
    ncat -kl -p 3333 -e ./Ch_Fuzz_1
    

    This runs the command as a network service, so you can connect from the Kali system over the network, and fuzz test the service.

    For example, from Kali you can connect to manually test it:

    nc ==edit:DESKTOP_IP_ADDRESS== 3333
    

    And use Spike to fuzz it.

    Alternatively, you can run the command directly.

    You can apply the techniques you have learned above to solve the challenge, to crash the program, which will then provide you with a flag.

    Flag: Find the flag hidden via each challenge program and submit them to Hacktivity!

    Conclusion

    At this point you have:

    • Manually audited C code for harder-to-spot errors
    • Used various fuzzers to test network programs for serious security flaws

    Well done!

    Next topic we will dive further into how these kinds of vulnerabilities can be exploited.