3 - Building the agent#

This section guides you through the process of implementing a kAFL agent, which includes both the harness and the specifics tailored for our Linux target.

Agent protocol#

The implementation of a kAFL agent can be broadly categorized into two main components:

  1. Initialization

  2. Harness

Initialization#

The initialization phase of the agent is responsible for:

  • Configuring the agent settings to optimize fuzzing behavior.

  • Mapping the payload buffer, which is shared among the Fuzzer, QEMU, and the VM.

  • Setting Intel PT filters to enhance coverage precision and execution speed.

  • Registering crash handlers to notify the fuzzer of target crashes and defining crash criteria.

  • Specifying IP ranges to inform the fuzzer about Intel PT ranges, crucial for obtaining accurate coverage.

Initialization Protocol
3. Set guest agent config
1. kAFL handshake
2. Query host config
4. Allocate payload buffer
5. Map payload buffer
6. Submit crash handlers
7. Submit Intel PT ranges
8. Submit CR3

Note

This protocol serves as a reference and is not strictly mandated.

However, Certain hypercalls must be executed in sequence.

Example: GET_HOST_CONFIG before SET_AGENT_CONFIG

The corresponding hypercalls to use:

  1. Handshake: ACQUIRE and RELEASE

  2. Query host config: GET_HOST_CONFIG

  3. Set agent config: SET_AGENT_CONFIG

  4. Map payload buffer: GET_PAYLOAD

  5. Submit crash handlers: SUBMIT_PANIC and SUBMIT_KASAN

  6. Submit Intel PT filters: RANGE_SUBMIT

  7. Submit CR3 (optional): SUBMIT_CR3

Note

For step 4 (Allocate payload buffer), there is no hypercall involved.

But please note that the payload buffer should be page-aligned.

Harness#

After the agent initialization is complete, the harness logic comes into play.

The harness performs the following sequence of operations:

  1. Write the next payload in the buffer: NEXT_PAYLOAD

  2. Start Intel PT coverage: ACQUIRE

  3. Call target function

  4. End Intel PT coverage, restore the guest snapshot: RELEASE

// 🔁 restore the snapshot and write next payload into the buffer
//		(take a snapshot on first call)
kAFL_hypercall(HYPERCALL_KAFL_NEXT_PAYLOAD, 0);
// 🟢 start coverage feedback collection
kAFL_hypercall(HYPERCALL_KAFL_ACQUIRE, 0);
// ⚡ call fuzz target with the buffer
target_entry(payload_buffer->data, payload_buffer->size);
// ⚪ stop coverage feedback collection
kAFL_hypercall(HYPERCALL_KAFL_RELEASE, 0);

DVKM target#

Kernel crash#

To handle kernel crashes with the kAFL agent for DVKM targets, we need to locate the Linux kernel crash routine and insert a PANIC hypercall at the relevant point.

Checkout the agent_tutorial branch of the kafl.linux repository.

It consists of 4 commits:

agent_tutorialarch/x86/include: add nyx_api.hprintk: replace _printk impl by kAFL hprintfpanic: insert kAFL PANIC hypercall in oops_exitkasan: insert kAFL KASAN hypercall in kasan_report
  1. Adds the nyx_api.h hypercall header to the Linux sources.

  2. Replaces the kernel’s printk implementation by our own own implementation based on HPRINTF hypercall.

  3. Inserts a PANIC hypercall in the oops_exit function of the kernel, invoked during the crash handling sequence.

  4. Inserts a KASAN hypercall in the kasan_report function of the kernel. Further discussion on enabling KASAN will appear in the tutorial’s improvements section.

Altered oops_exit handler#
 1/*
 2 * Called when the architecture exits its oops handler, after printing
 3 * everything.
 4 */
 5void oops_exit(void)
 6{
 7	do_oops_enter_exit();
 8	print_oops_end_marker();
 9	kmsg_dump(KMSG_DUMP_OOPS);
10
11	kAFL_hypercall(HYPERCALL_KAFL_PANIC, 0);
12}

Initialization#

The kAFL agent’s remaining code resides in userland, within test_dvkm.c

Keys points:

The payload buffer is page-aligned using aligned_alloc.

Payload buffer paged-aligned allocation#
kAFL_payload* payload_buffer = aligned_alloc((size_t)sysconf(_SC_PAGESIZE), host_config.payload_buffer_size);

We ensure that the payload is in resident memory with mlock()

Locking payload buffer in resident memory#
mlock(payload_buffer, host_config.payload_buffer_size);

The IP ranges are identified by parsing /proc/modules, for the dvkm module

Detecting IP ranges for dvkm module#
detectranges("/proc/modules", "dvkm");
...
static int detectranges(char *mapfile, char *pattern) {
    // dvkm 24576 0 - Live 0xffffffffc0201000 (O)
    ret = sscanf(line, "%s %lu %d - %s %lx", module_name, &module_size, &instances_loaded, load_state, &kernel_offset);
}

Harness#

The harness is constructed around the ioctl() function call:

DVKM Harness#
kAFL_hypercall(HYPERCALL_KAFL_NEXT_PAYLOAD, 0);
// prepare ioctl code and io_buffer struct range[0-0xC]
ioctl_code = payload_buffer->data[0] % 0xD;
ioctl_num = IOCTL(ioctl_code);
// write width, height and datasize
size_t write_size = sizeof(struct dvkm_obj) - sizeof(io_buffer.data);
memcpy((void*)&io_buffer, &payload_buffer[1], write_size);
// assign rest of payload_buffer to io_buffer.data
io_buffer.data = (char*)&payload_buffer->data[write_size+1];
// struct is now ready
kAFL_hypercall(HYPERCALL_KAFL_ACQUIRE, 0);
ioctl(fd, ioctl_num, &io_buffer);
kAFL_hypercall(HYPERCALL_KAFL_RELEASE, 0);
  • the ioctl_code is generated from the first payload byte, and modulo 0xD ensures a valid IOCTL.

  • write_size is calculated to only write the relevant fields (width, height and datasize) of the payload buffer.

  • the remaining payload buffer fills the data pointer field.

For the dvkm.c module, we’ve limited the INFO() printk format strings to prevent output congestion during fuzzing:

int Use_after_free_IOCTL_Handler(struct dvkm_obj *io)
{
    INFO("[+] data: %.50s\n", kernel_data_buffer);
}

Congratulations! You now have a comprehensive understanding of the kAFL agent tailored for the DVKM target.

Proceed to the next section to commence your fuzzing campaign.