Skip to content

Commit

Permalink
Add exit codes to process destroy information (#36)
Browse files Browse the repository at this point in the history
* Working on adding exit codes, need to bump usersim version

* Update submodules

* Add tests and docs, try to fix annotation failure seen in CI

* Updating usersim to pick up SAL fix
  • Loading branch information
Austin-Lamb authored May 2, 2024
1 parent 9ec132f commit dd0746b
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 30 deletions.
52 changes: 52 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ We'd love your help with eBPF for Windows! Here are our contribution guidelines.
- [Code of Conduct](#code-of-conduct)
- [Bugs](#bugs)
- [New Features](#new-features)
- [Building the code](#building-the-code)
- [Testing the code](#testing-the-code)
- [Contributor License Agreement](#contributor-license-agreement)
- [Contributing Code](#contributing-code)
- [Tests](#tests)
Expand Down Expand Up @@ -56,6 +58,56 @@ communicate your proposal so that the community can review and provide feedback.
early feedback will help ensure your implementation work is accepted by the community.
This will also allow us to better coordinate our efforts and minimize duplicated effort.

## Building the code

To build locally, ensure your environment is set up:

1. Install Visual Studio 2022 with at least the "Desktop development with C++" workload
1. Install the Windows SDK 10.0.22621.0 with `winget install Microsoft.WindowsSDK.10.0.22621`
1. Install the Windows DDK 10.0.22621.0 with `winget install Microsoft.WindowsWDK.10.0.22621`

Then do one-time repo setup:

1. Open a Visual Studio 2022 Developer Command Prompt
1. cd `<root of your clone>`
1. `powershell -file scripts\initialize_repo.ps1`

Then you can build normally in Visual Studio:

1. Open `ntosebpfext.sln` in Visual Studio. You may want to run VS as admin if you want to debug in VS.

## Testing the code

### Unit tests

Run the unit tests by going to the binaries output folder (ex: `x64\Debug`) and running `ntosebpfext_unit.exe -d yes`

### E2E tests

The end-to-end tests use a tool called `process_monitor` to take data from the `ntosebpfext` extension and place it in a ring buffer that is visible from user-mode (this happens in `process_monitor.sys`). Then the `process_monitor.exe` user-mode process prints the events it sees to a file that the tests verify.

To run E2E tests you'll need to install eBPF for Windows and the ntosebpfext extension driver locally.

1. Open a command prompt as admin
1. `cd <your binaries folder>` (ex: `<root of your clone>\x64\Debug`)
1. `powershell .\Install-eBpfForWindows.ps1 0.16.0`
1. `powershell .\Test-ProcessMonitor.ps1`

### Debugging locally

1. Install eBPF for Windows locally
1. Install `ntosebpfext` locally from an admin command prompt: `sc create ntosebpfext type=kernel start=auto binpath="<your binaries folder>\ntosebpfext\ntosebpfext.sys`
1. Run Visual Studio as admin
1. Choose something like `process_monitor` as the startup project and debug.

Note that when you do this, the `ntosebpfext.sys` driver will be loaded, so if you rebuild the solution, it will fail to build because it can't overwrite the in-use driver. For this you have a couple of options:

1. `sc stop ntosebpfxt`
1. Build the solution however you like
1. `sc start ntosebpfext`

Or you can just run `scripts\rebuild_ntosebpfext.cmd` which does those 3 steps.

## Contributor License Agreement

You will need to complete a Contributor License Agreement (CLA) for any code submissions.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [[email protected]](mailto:[email protected]) with any additional questions or comments.

For more details on how to build and test, see [The contribution docs](CONTRIBUTING.md)

## Trademarks

This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
Expand Down
2 changes: 1 addition & 1 deletion external/usersim
Submodule usersim updated 1 files
+1 −2 inc/usersim/ps.h
15 changes: 8 additions & 7 deletions include/ebpf_ntos_hooks.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ typedef enum _process_operation

typedef struct _process_md
{
uint8_t* command_start; ///< Pointer to start of the command line as UTF-8 string.
uint8_t* command_end; ///< Pointer to end of the command line as UTF-8 string.
uint64_t process_id; ///< Process ID.
uint64_t parent_process_id; ///< Parent process ID.
uint64_t creating_process_id; ///< Creating process ID.
uint64_t creating_thread_id; ///< Creating thread ID.
process_operation_t operation; ///< Operation to do.
uint8_t* command_start; ///< Pointer to start of the command line as UTF-8 string.
uint8_t* command_end; ///< Pointer to end of the command line as UTF-8 string.
uint64_t process_id; ///< Process ID.
uint64_t parent_process_id; ///< Parent process ID.
uint64_t creating_process_id; ///< Creating process ID.
uint64_t creating_thread_id; ///< Creating thread ID.
uint32_t process_exit_code; ///< Process exit status.
process_operation_t operation : 8; ///< Operation to do.
} process_md_t;

/*
Expand Down
1 change: 1 addition & 0 deletions ntosebpfext/ntos_ebpf_ext_process.c
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ _ebpf_process_create_process_notify_routine_ex(
} else {
process_notify_context.process_md.operation = PROCESS_OPERATION_DELETE;
process_notify_context.process_md.process_id = (uint64_t)process_id;
process_notify_context.process_md.process_exit_code = PsGetProcessExitStatus(process);
}

// For each attached client call the process hook.
Expand Down
6 changes: 6 additions & 0 deletions scripts/rebuild_ntosebpfext.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
rem Copyright (c) Microsoft Corporation
rem SPDX-License-Identifier: MIT

sc stop ntosebpfext
msbuild /m /t:Rebuild ntosebpfext.sln /p:Configuration=Debug /p:Platform=x64
sc start ntosebpfext
70 changes: 68 additions & 2 deletions tests/ntosebpfext_unit/ntos_ebpfext_unit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,81 @@ TEST_CASE("process_invoke", "[ntosebpfext]")
REQUIRE((HANDLE)client_context.process_context.parent_process_id == create_info.ParentProcessId);
REQUIRE((HANDLE)client_context.process_context.creating_process_id == create_info.CreatingThreadId.UniqueProcess);
REQUIRE((HANDLE)client_context.process_context.creating_thread_id == create_info.CreatingThreadId.UniqueThread);
REQUIRE(client_context.process_context.process_exit_code == 0); // Should be 0 for creation events
REQUIRE(create_info.CreationStatus == STATUS_ACCESS_DENIED);
REQUIRE(client_context.process_context.operation == PROCESS_OPERATION_CREATE);
REQUIRE((int)client_context.process_context.operation == PROCESS_OPERATION_CREATE);

// Test process termination.
// Just verify that it doesn't crash.
usersime_invoke_process_creation_notify_routine(reinterpret_cast<PEPROCESS>(&fake_eprocess), (HANDLE)1, nullptr);

REQUIRE(client_context.process_context.process_id == 1);
REQUIRE(client_context.process_context.operation == PROCESS_OPERATION_DELETE);
// The exit code should be -1 in tests, becaus we haven't set up a callback to specify the exit code (see the
// process exit codes test below for that).
REQUIRE(client_context.process_context.process_exit_code == -1);
REQUIRE((int)client_context.process_context.operation == PROCESS_OPERATION_DELETE);
}

TEST_CASE("process exit codes", "[ntosebpfext]")
{
ebpf_extension_data_t npi_specific_characteristics = {};
test_process_client_context_t client_context = {};

ntosebpf_ext_helper_t helper(
&npi_specific_characteristics,
(_ebpf_extension_dispatch_function)ntosebpfext_unit_invoke_process_program,
(ntosebpfext_helper_base_client_context_t*)&client_context);

// Test process creation.
std::wstring process_name = L"notepad.exe";
std::wstring command_line = L"notepad.exe foo.txt";
UNICODE_STRING process_name_unicode = {};
UNICODE_STRING command_line_unicode = {};

PS_CREATE_NOTIFY_INFO create_info = {};
create_info.CommandLine = &command_line_unicode;
create_info.ImageFileName = &process_name_unicode;
create_info.ParentProcessId = (HANDLE)4;
create_info.CreatingThreadId.UniqueProcess = (HANDLE)5;
create_info.CreatingThreadId.UniqueThread = (HANDLE)6;
create_info.CreationStatus = STATUS_SUCCESS;

RtlInitUnicodeString(&process_name_unicode, process_name.c_str());
RtlInitUnicodeString(&command_line_unicode, command_line.c_str());

struct
{
uint64_t some_value;
} fake_eprocess = {};

usersime_invoke_process_creation_notify_routine(
reinterpret_cast<PEPROCESS>(&fake_eprocess), (HANDLE)1, &create_info);

const int expectedExitCode = 118;

usersime_set_process_exit_status_callback([](PEPROCESS process) -> NTSTATUS { return expectedExitCode; });

std::string test_command_line = std::string(
reinterpret_cast<char*>(client_context.process_context.command_start),
reinterpret_cast<char*>(client_context.process_context.command_end));

REQUIRE(test_command_line == std::string("notepad.exe foo.txt"));

REQUIRE(client_context.process_context.process_id == 1);
REQUIRE((HANDLE)client_context.process_context.parent_process_id == create_info.ParentProcessId);
REQUIRE((HANDLE)client_context.process_context.creating_process_id == create_info.CreatingThreadId.UniqueProcess);
REQUIRE((HANDLE)client_context.process_context.creating_thread_id == create_info.CreatingThreadId.UniqueThread);
REQUIRE(client_context.process_context.process_exit_code == 0); // Should be 0 for creation events
REQUIRE(create_info.CreationStatus == STATUS_ACCESS_DENIED);
REQUIRE((int)client_context.process_context.operation == PROCESS_OPERATION_CREATE);

// Test process termination.
// Just verify that it doesn't crash.
usersime_invoke_process_creation_notify_routine(reinterpret_cast<PEPROCESS>(&fake_eprocess), (HANDLE)1, nullptr);

REQUIRE(client_context.process_context.process_id == 1);
REQUIRE(client_context.process_context.process_exit_code == expectedExitCode);
REQUIRE((int)client_context.process_context.operation == PROCESS_OPERATION_DELETE);
}

#pragma endregion process
44 changes: 34 additions & 10 deletions tools/process_monitor/Test-ProcessMonitor.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,15 @@ if ($service_status.Status -ne "Running") {
exit 1
}

# Ensure the output txt file is deleted before we run, in case this test suite is being run locally for development/debugging
$outputFilePath = ".\process_monitor_output.txt";
if (Test-Path $outputFilePath)
{
Remove-Item $outputFilePath
}

# Start Process_Montior.exe, redirect the output to a file.
Start-Process -FilePath ".\Process_Monitor.exe" -RedirectStandardOutput ".\process_monitor_output.txt" -PassThru -RedirectStandardError ".\process_monitor_error.txt"0.15.0
Start-Process -FilePath ".\Process_Monitor.exe" -RedirectStandardOutput $outputFilePath -PassThru #-RedirectStandardError $outputFilePath+".err"

# Wait for the Process Monitor to start.
Start-Sleep -Seconds 5
Expand All @@ -48,38 +55,55 @@ if (Get-Process -name Process_Monitor) {

# Start a test process.
Start-Process -FilePath "cmd.exe" -ArgumentList "/c echo Hello World" -Wait
Start-Process -FilePath "cmd.exe" -ArgumentList "/c exit 1" -Wait
Start-Process -FilePath "cmd.exe" -ArgumentList "/c exit 235" -Wait

# Wait for the Process Monitor to capture the process.
# Wait for the Process Monitor to capture the processes.
Start-Sleep -Seconds 5

# Stop Process_Monitor.exe
Get-Process -name Process_Monitor | stop-process

# Print the output file content for debugging.
Write-Output "Process Monitor output file content:"
Get-Content -Path "process_monitor_output.txt"
Get-Content -Path $outputFilePath

# Check if the output file is created.
if (Test-Path -Path "process_monitor_output.txt") {
if (Test-Path -Path $outputFilePath) {
Write-Output "Process Monitor output file is created."
} else {
Write-Output "Process Monitor output file is not created."
exit 1
}

# Check for the process name in the output file.
if ((Get-Content -Path "process_monitor_output.txt") -match "cmd.exe") {
Write-Output "Process Monitor output file contains the expected string."
if ((Get-Content -Path $outputFilePath) -match "cmd.exe") {
Write-Output "Process Monitor output file contains the expected string (cmd.exe)."
} else {
Write-Output "Process Monitor output file does not contain the expected string."
Write-Output "Process Monitor output file does not contain the expected string (cmd.exe)."
exit 1
}

# Check for the process command in the output file.
if ((Get-Content -Path "process_monitor_output.txt") -match "/c echo Hello World ") {
Write-Output "Process Monitor output file contains the expected string."
if ((Get-Content -Path $outputFilePath) -match "/c echo Hello World ") {
Write-Output "Process Monitor output file contains the expected string (/c echo Hello World )."
} else {
Write-Output "Process Monitor output file does not contain the expected string (/c echo Hello World )."
exit 1
}

# Check that we saw the error codes correctly flowing through
if ((Get-Content -Path $outputFilePath) -match ", exit code: 1 ") {
Write-Output "Process Monitor output file contains the expected string (, exit code: 1 )."
} else {
Write-Output "Process Monitor output file does not contain the expected string (, exit code: 1 )."
exit 1
}

if ((Get-Content -Path $outputFilePath) -match ", exit code: 235 ") {
Write-Output "Process Monitor output file contains the expected string (, exit code: 235 )."
} else {
Write-Output "Process Monitor output file does not contain the expected string."
Write-Output "Process Monitor output file does not contain the expected string (, exit code: 235 )."
exit 1
}

Expand Down
22 changes: 13 additions & 9 deletions tools/process_monitor/bpf/process_monitor.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ typedef struct
uint64_t parent_process_id;
uint64_t creating_process_id;
uint64_t creating_thread_id;
uint32_t process_exit_code;
uint64_t operation;
} process_info_t;

Expand Down Expand Up @@ -54,15 +55,18 @@ SEC("process")
int
ProcessMonitor(process_md_t* ctx)
{
process_info_t process_info = {
.process_id = ctx->process_id,
.parent_process_id = ctx->parent_process_id,
.creating_process_id = ctx->creating_process_id,
.creating_thread_id = ctx->creating_thread_id,
.operation = ctx->operation,
};

if (ctx->operation == PROCESS_OPERATION_CREATE) {
process_info_t process_info;

memset(&process_info, 0, sizeof(process_info));

process_info.process_id = ctx->process_id;
process_info.parent_process_id = ctx->parent_process_id;
process_info.creating_process_id = ctx->creating_process_id;
process_info.creating_thread_id = ctx->creating_thread_id;
process_info.process_exit_code = ctx->process_exit_code;
process_info.operation = ctx->operation;

if (process_info.operation == PROCESS_OPERATION_CREATE) {
uint8_t buffer[MAX_PATH];

memset(buffer, 0, sizeof(buffer));
Expand Down
4 changes: 3 additions & 1 deletion tools/process_monitor/process_monitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ typedef struct
uint64_t parent_process_id;
uint64_t creating_process_id;
uint64_t creating_thread_id;
uint32_t process_exit_code;
uint64_t operation;
} process_info_t;

Expand Down Expand Up @@ -71,7 +72,8 @@ process_monitor_history_callback(void* ctx, void* data, size_t size)
break;
}
case PROCESS_OPERATION_DELETE: {
std::cout << "Process deleted: " << event->process_id << "\n" << file_name << std::endl;
std::cout << "Process deleted: " << event->process_id << ", exit code: " << event->process_exit_code << " \n"
<< file_name << std::endl;
break;
}
default:
Expand Down

0 comments on commit dd0746b

Please sign in to comment.