Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions tests/ntosebpfext/ntosebpfext_unit/ntos_ebpfext_unit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <map>
#include <sddl.h>
#include <stop_token>
#include <thread>
#pragma warning(push)
Expand Down Expand Up @@ -785,4 +786,171 @@ TEST_CASE("process_resolve_account", "[ntosebpfext]")
}
}

static std::atomic<uint32_t> sid_event_count = 0;
static uint32_t sid_event_sid_size = 0;
static uint8_t sid_event_sid[TOKEN_SID_MAX_SIZE] = {};

static int
sid_ringbuf_event_callback(void* ctx, void* data, size_t size)
{
UNREFERENCED_PARAMETER(ctx);

if (size != sizeof(process_info_t)) {
return 0;
}

process_info_t* info = (process_info_t*)data;
if (info->process_id != TEST_PROCESS_ID) {
return 0;
}

sid_event_sid_size = info->token_sid_size;
memcpy(sid_event_sid, info->token_sid, TOKEN_SID_MAX_SIZE);
sid_event_count++;
Comment thread
LakshK98 marked this conversation as resolved.
return 0;
}

TEST_CASE("process_sid_to_user_mode_api", "[ntosebpfext]")
{
// This test verifies that a SID written by the BPF program (kernel side) into the
// ring buffer can be read in user mode and successfully passed to the Windows API
// GetSidSubAuthorityCount.

// Reset shared state.
sid_event_count = 0;
sid_event_sid_size = 0;
memset(sid_event_sid, 0, sizeof(sid_event_sid));

driver_service ntosebpfext_driver;
REQUIRE(
ntosebpfext_driver.create(L"ntosebpfext", driver_service::get_driver_path("ntosebpfext.sys").c_str()) == true);
REQUIRE(ntosebpfext_driver.start() == true);
auto cleanup_driver = wil::scope_exit([&]() {
ntosebpfext_driver.stop();
ntosebpfext_driver.unload();
});

struct bpf_object* object = bpf_object__open("process_monitor.sys");
REQUIRE(object != nullptr);
auto cleanup_object = wil::scope_exit([&]() { bpf_object__close(object); });

REQUIRE(bpf_object__load(object) == 0);

bpf_program* process_monitor = bpf_object__find_program_by_name(object, "ProcessMonitor");
REQUIRE(process_monitor != nullptr);

fd_t process_program_fd = bpf_program__fd(process_monitor);
REQUIRE(process_program_fd != ebpf_fd_invalid);

// Set up ring buffer with the SID-capturing callback.
bpf_map* process_ringbuf_map = bpf_object__find_map_by_name(object, "process_ringbuf");
REQUIRE(process_ringbuf_map != nullptr);
int process_ringbuf_fd = bpf_map__fd(process_ringbuf_map);
REQUIRE(process_ringbuf_fd != ebpf_fd_invalid);

ebpf_ring_buffer_opts ring_opts = {.sz = sizeof(ebpf_ring_buffer_opts), .flags = EBPF_RINGBUF_FLAG_AUTO_CALLBACK};
ring_buffer* process_ring_buffer =
ebpf_ring_buffer__new(process_ringbuf_fd, sid_ringbuf_event_callback, nullptr, &ring_opts);
REQUIRE(process_ring_buffer != nullptr);
auto cleanup_ring_buffer = wil::scope_exit([&]() { ring_buffer__free(process_ring_buffer); });

// Build the input context with a well-known SID (Local System: S-1-5-18).
uint8_t test_sid[] = {
0x01, // Revision
0x01, // SubAuthorityCount
0x00,
0x00,
0x00,
0x00,
0x00,
0x05, // IdentifierAuthority (NT Authority)
0x12,
0x00,
0x00,
0x00 // SubAuthority[0] = 18 (SECURITY_LOCAL_SYSTEM_RID)
};

std::wstring command_line = L"cmd.exe /c echo test";
std::wstring image_path = L"C:\\Windows\\System32\\cmd.exe";

test_process_notify_context_t process_ctx_in = {0};
test_process_notify_context_t process_ctx_out = {0};

process_ctx_in.process_md.process_id = TEST_PROCESS_ID;
process_ctx_in.process_md.parent_process_id = 1;
process_ctx_in.process_md.operation = PROCESS_OPERATION_CREATE;
process_ctx_in.process_md.token_sid_size = sizeof(test_sid);
memcpy(process_ctx_in.process_md.token_sid, test_sid, sizeof(test_sid));

process_ctx_in.command_line.Length = static_cast<USHORT>(command_line.length() * sizeof(wchar_t));
process_ctx_in.command_line.MaximumLength = process_ctx_in.command_line.Length;
process_ctx_in.command_line.Buffer = NULL;

process_ctx_in.image_file_name.Length = static_cast<USHORT>(image_path.length() * sizeof(wchar_t));
process_ctx_in.image_file_name.MaximumLength = process_ctx_in.image_file_name.Length;
process_ctx_in.image_file_name.Buffer = NULL;

process_ctx_in.account_name.Length = 0;
process_ctx_in.account_name.MaximumLength = 0;
process_ctx_in.account_domain.Length = 0;
process_ctx_in.account_domain.MaximumLength = 0;

size_t total_data_size = static_cast<size_t>(process_ctx_in.command_line.Length) +
static_cast<size_t>(process_ctx_in.image_file_name.Length);
std::vector<uint8_t> packed_data(total_data_size);

size_t offset = 0;
memcpy(packed_data.data() + offset, command_line.c_str(), process_ctx_in.command_line.Length);
offset += process_ctx_in.command_line.Length;
memcpy(packed_data.data() + offset, image_path.c_str(), process_ctx_in.image_file_name.Length);

std::vector<uint8_t> data_out_buffer(total_data_size);

bpf_test_run_opts bpf_opts = {0};
bpf_opts.repeat = 1;
bpf_opts.ctx_in = &process_ctx_in;
bpf_opts.ctx_size_in = sizeof(process_ctx_in);
bpf_opts.ctx_out = &process_ctx_out;
bpf_opts.ctx_size_out = sizeof(process_ctx_out);
bpf_opts.data_in = packed_data.data();
bpf_opts.data_size_in = static_cast<uint32_t>(total_data_size);
bpf_opts.data_out = data_out_buffer.data();
bpf_opts.data_size_out = static_cast<uint32_t>(total_data_size);

// Run the BPF program which writes process_info_t (including the SID) to the ring buffer.
REQUIRE(bpf_prog_test_run_opts(process_program_fd, &bpf_opts) == 0);

// Wait for the ring buffer callback to capture the SID.
for (int i = 0; i < 5; i++) {
if (sid_event_count > 0) {
break;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
REQUIRE(sid_event_count > 0);

// The SID has arrived in user mode via the ring buffer.
// Validate it solely through user-mode Windows APIs — no comparison against the input bytes.
REQUIRE(sid_event_sid_size > 0);

PSID received_sid = (PSID)sid_event_sid;
REQUIRE(IsValidSid(received_sid));
REQUIRE(GetLengthSid(received_sid) == sid_event_sid_size);

// Use GetSidSubAuthorityCount to inspect the SID received from the ring buffer.
// The Local System SID (S-1-5-18) has 1 sub-authority.
PUCHAR sub_authority_count = GetSidSubAuthorityCount(received_sid);
REQUIRE(sub_authority_count != nullptr);
REQUIRE(*sub_authority_count == 1);

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test currently only asserts that the received SID is valid and has SubAuthorityCount == 1. This would still pass if the program/user-mode plumbing returned a different valid SID that happens to have one sub-authority (e.g., S-1-5-19), so it doesn’t fully verify that the intended SID (Local System: S-1-5-18) was received. Consider additionally validating the identifier authority and the first sub-authority value via Windows APIs (e.g., GetSidIdentifierAuthority / GetSidSubAuthority or ConvertSidToStringSidW), without comparing against the original input bytes.

Suggested change
REQUIRE(*sub_authority_count == 1);
REQUIRE(*sub_authority_count == 1);
// Fully validate that the SID received in user mode is Local System (S-1-5-18),
// not just any valid SID with one sub-authority.
PSID_IDENTIFIER_AUTHORITY identifier_authority = GetSidIdentifierAuthority(received_sid);
REQUIRE(identifier_authority != nullptr);
REQUIRE(memcmp(identifier_authority->Value, SECURITY_NT_AUTHORITY.Value, sizeof(identifier_authority->Value)) == 0);
PDWORD first_sub_authority = GetSidSubAuthority(received_sid, 0);
REQUIRE(first_sub_authority != nullptr);
REQUIRE(*first_sub_authority == SECURITY_LOCAL_SYSTEM_RID);

Copilot uses AI. Check for mistakes.

// Convert the SID to a string representation using ConvertSidToStringSidW.
LPWSTR sid_string = nullptr;
REQUIRE(ConvertSidToStringSidW(received_sid, &sid_string));
REQUIRE(sid_string != nullptr);
auto sid_string_guard = wil::scope_exit([&]() { LocalFree(sid_string); });

std::wcout << L" SID from ring buffer: " << sid_string << std::endl;
REQUIRE(wcscmp(sid_string, L"S-1-5-18") == 0);
}

#pragma endregion process
Loading