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:
Initialization
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.
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:
Handshake:
ACQUIRE
andRELEASE
Query host config:
GET_HOST_CONFIG
Set agent config:
SET_AGENT_CONFIG
Map payload buffer:
GET_PAYLOAD
Submit crash handlers:
SUBMIT_PANIC
andSUBMIT_KASAN
Submit Intel PT filters:
RANGE_SUBMIT
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:
Write the next payload in the buffer:
NEXT_PAYLOAD
Start Intel PT coverage:
ACQUIRE
Call target function
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:
Adds the
nyx_api.h
hypercall header to the Linux sources.Replaces the kernel’s printk implementation by our own own implementation based on HPRINTF hypercall.
Inserts a PANIC hypercall in the
oops_exit
function of the kernel, invoked during the crash handling sequence.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.
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
.
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()
mlock(payload_buffer, host_config.payload_buffer_size);
The IP ranges are identified by parsing /proc/modules
, for the 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:
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 modulo0xD
ensures a valid IOCTL.write_size
is calculated to only write the relevant fields (width
,height
anddatasize
) 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.