In part 1 we have showed how to build bof-launcher project and how to write/debug very simple BOF.

In this blog post we will show how to:

  • Write a bit more complicated BOF that expects arguments passed by the user
  • Run our new BOF using integration_with_c example program
  • Execute BOF programmatically using bof-launcher library
  • Use BOFs as plugins (bofObjectGetProcAddress())

BOF with arguments

Using C:

#include "beacon.h"

unsigned char go(unsigned char* arg_data, int arg_len) {
    datap parser = { 0 };
    BeaconDataParse(&parser, arg_data, arg_len);

    const char* user_str = BeaconDataExtract(&parser, NULL);
    int user_int = BeaconDataInt(&parser);
    int user_short = BeaconDataShort(&parser);
 
    BeaconPrintf(0,
        "BOF with args received - string: %s, int: %d, short: %d\n", 
        user_str, user_int, user_short);

    return 0;
}

Using Zig:

const beacon = @import("bof_api").beacon;

pub export fn go(arg_data: ?[*]u8, arg_len: i32) callconv(.C) u8 {
    var parser = beacon.datap{};
    beacon.dataParse(&parser, arg_data, arg_len);

    const user_str = beacon.dataExtract(&parser, null);
    const user_int = beacon.dataInt(&parser);
    const user_short = beacon.dataShort(&parser);
 
    _ = beacon.printf(0,
        "BOF with args received - string: %s, int: %d, short: %d\n",
        user_str, user_int, user_short);

    return 0;
}

Take a look at part 1 to see how to build above BOFs.

To pass arguments and run BOFs you can use integration_with_c example program:

.\zig-out\bin\integration_with_c_win_x64.exe .\zig-out\bin\bofWithArgs.coff.x64.o "test str" i:12 s:345
.\zig-out\bin\integration_with_c_win_x64.exe .\zig-out\bin\bofWithArgsC.coff.x64.o "test str" i:12 s:345

Using bof-launcher library

bof-launcher library is the core component of the project. It is built as a static library for all supported targets when you run zig build. After building binaries will show up in zig-out/lib directory:

bof_launcher_win_x64.lib
bof_launcher_win_x86.lib
libbof_launcher_lin_aarch64.a
libbof_launcher_lin_arm.a
libbof_launcher_lin_x64.a
libbof_launcher_lin_x86.a

C header file can be found here: bof-launcher/src/bof_launcher_api.h.

Now, lets take a look how to run above BOFs programmatically using bof-launcher C API:

  • Load object files from filesystem to memory using libc or native OS API.
  • Parse, relocate and resolve external symbols with bofObjectInitFromMemory():
BofObjectHandle bof_handle;
bofObjectInitFromMemory(obj_file_data, obj_file_len, &bof_handle);
  • You can now execute BOF as many times as you want with bofObjectRun() function. In a simplest form when BOF doesn’t take any arguments code is trivial:
BofContext* bof_context = NULL;
bofObjectRun(bof_handle, NULL, 0, &bof_context);
  • To pass arguments we need to create BofArgs object (note: all arguments are passed as strings and parsed by bofArgsAdd() function):
BofArgs* args = NULL;
bofArgsInit(&args);

bofArgsBegin(args);
bofArgsAdd(args, "test str", 8); // no prefix means string, string length: 8
bofArgsAdd(args, "i:12", 4); // 'i:' prefix means int, string length: 4
bofArgsAdd(args, "s:345", 5); // 's:' prefix means short, string length: 5
bofArgsEnd(args);
  • To run BOF with above arguments, do:
BofContext* bof_context = NULL;
bofObjectRun(
    bof_handle,
    bofArgsGetBuffer(args),
    bofArgsGetBufferSize(args),
    &bof_context);
  • BofContext object is created whenever BOF is executed (i.e. bofObjectRun() is run), it stores the content of BOF’s output and its exit code status:
int exit_code = bofContextGetExitCode(bof_context);
const char* output = bofContextGetOutput(bof_context, NULL);
  • The last step is to release resources (note: you can re-use BofObjectHandle and BofArgs objects for another executions):
bofObjectRelease(bof_handle);
bofArgsRelease(args);
bofContextRelease(bof_context);

Note that having, such fine grained API allows for plenty of interesting modes of operations and use cases:

  1. Using bofObjectInitFromMemory it is possible to load and store multiple different BOFs in memory at once, each identified by its unique handle (BofObjectHandle type).
  2. You can then run chosen BOF multiple times with bofObjectRun (or one of “sister” functions bofObjectRunAsyncThread() and bofObjectRunAsyncProcess) for different targets and conveniently compare results between each run by inspecting output stored in BofContext objects returned from every run.
  3. You can pipe output from one BOF (stored in its BofContext object) as input to the other already loaded BOF: first create bofArgs object from the first BOF’s output, then pass it as a BOF input to second BOF (with bofArgsGetBuffer() function) during execution. You don’t have to stop there, you could pass the second’s BOF output as an input to a third BOF if desired, effectively implementing BOF chain as we call it - conceptually similar to Bash Pipelines but done with BOFs and entirely in-memory.
  4. You can explicitly unload chosen BOF from the memory with the bofObjectRelease when you know it won’t by needed anymore.

For more details, take a look at bof-launcher/src/bof_launcher_api.h for a description of each function that bof-launcher library provides.

BOFs as plugins or API-style BOFs

BOF can contain any number of functions. Address of every non-static function can be retrieved with bofObjectGetProcAddress() API. For example, consider following BOF:

#include "beacon.h"

void connect(const char* url) {
    // ...
}

void send(const void* data) {
    // ...
}

unsigned char go(unsigned char* arg_data, int arg_len) {
    // does nothing
    return 0;
}

Load BOF to the address space of a calling process:

BofObjectHandle bof_handle;
bofObjectInitFromMemory(obj_file_data, obj_file_len, &bof_handle);

Get and use function pointers:

void (*connect)(const char*) =
    (void(*)(const char*))bofObjectGetProcAddress(bof_handle, "connect");

void (*send)(const void*) =
    (void(*)(const void*))bofObjectGetProcAddress(bof_handle, "send");

connect("127.0.0.1");
send(data);

For a real-life example of API-style BOF, see our kmodLoader BOF.

This way you can treat BOFs as plugins. Different BOFs can implement a set of functions (interfaces) and be used as building blocks for a bigger system. Each BOF can be downloaded on-demand to provide needed functionality.

Summary

In the third part of this blog post series we will show how to programmatically run BOFs in a separate thread and/or in a separate process which can be useful for long running or risky BOFs. We will also take a closer look on BOFs that we have implemented.