Write, debug and execute BOFs using bof-launcher (part 2)
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 bybofArgsAdd()
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
andBofArgs
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:
- Using
bofObjectInitFromMemory
it is possible to load and store multiple different BOFs in memory at once, each identified by its unique handle (BofObjectHandle
type). - You can then run chosen BOF multiple times with
bofObjectRun
(or one of “sister” functionsbofObjectRunAsyncThread()
andbofObjectRunAsyncProcess
) for different targets and conveniently compare results between each run by inspecting output stored inBofContext
objects returned from every run. - You can pipe output from one BOF (stored in its
BofContext
object) as input to the other already loaded BOF: first createbofArgs
object from the first BOF’s output, then pass it as a BOF input to second BOF (withbofArgsGetBuffer()
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. - 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.