Finished tracer logic and command environment data

This commit is contained in:
York Jasper Niebuhr 2025-08-16 22:05:07 +02:00
parent 528eccc626
commit 9d3166a657
6 changed files with 674 additions and 9 deletions

88
include/environment.hpp Normal file
View File

@ -0,0 +1,88 @@
#pragma once
#include <unordered_map>
#include <unistd.h>
#include <memory>
#include <sys/user.h>
#include "wirekit.hpp"
struct TraceContext {
enum class EXECUTION_MODE { STARTING, USER, KERNEL /* Currently executing a system call */ };
pid_t pid;
EXECUTION_MODE mode;
user_regs_struct regs;
bool regs_dirty;
};
class TraceContextCollection {
std::unordered_map<pid_t, std::unique_ptr<TraceContext>> m_contexts;
pid_t m_active;
public:
TraceContextCollection(const TraceContextCollection& other) = delete;
TraceContextCollection(TraceContextCollection&& other);
TraceContextCollection& operator=(const TraceContextCollection& other) = delete;
TraceContextCollection& operator=(TraceContextCollection&& other);
TraceContextCollection();
~TraceContextCollection();
bool add_context(pid_t pid);
void delete_context(pid_t pid);
void clear();
TraceContext* context(pid_t pid);
const TraceContext* context(pid_t pid) const;
void context_set_active(pid_t pid);
void context_clear_active();
TraceContext* context_get_active();
const TraceContext* context_get_active() const;
};
class Environment {
struct HookPair {
hook_t entry = nullptr;
hook_t exit = nullptr;
};
std::unordered_map<reg_t, HookPair> m_hooks;
HookPair m_default_hooks;
TraceContextCollection m_trace_contexts;
public:
Environment(const Environment& other) = delete;
Environment(Environment&& other) = delete;
Environment& operator=(const Environment& other) = delete;
Environment& operator=(Environment&& other) = delete;
Environment();
~Environment();
void register_hooks(reg_t syscall, hook_t entry, hook_t exit);
void register_default_hooks(hook_t entry, hook_t exit);
bool entry_hook(reg_t syscall) const;
bool exit_hook(reg_t syscall) const;
bool default_entry_hook() const;
bool default_exit_hook() const;
TraceContextCollection& contexts();
const TraceContextCollection& contexts() const;
void clear();
};
// An EnvironmentScope calls Environment::clear() when destructed
class EnvironmentScope {
Environment& m_environment;
public:
EnvironmentScope(const EnvironmentScope& other) = delete;
EnvironmentScope(EnvironmentScope&& other) = delete;
EnvironmentScope& operator=(const EnvironmentScope& other) = delete;
EnvironmentScope& operator=(EnvironmentScope&& other) = delete;
EnvironmentScope(Environment& environment);
~EnvironmentScope();
};
extern Environment ENV;

21
include/interface.hpp Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include "wirekit.hpp"
extern err_t(*wirekit_prepare_handle)();
extern err_t(*wirekit_command_start_handle)(int, const char* const*);
extern void(*wirekit_command_exit_handle)();
extern void(*wirekit_subject_start_handle)();
extern void(*wirekit_subject_exit_handle)();
// Exposure to wirekit is achieved by adding these functions to rewire's symbol tables
#define EXPOSED __attribute__((visibility("default")))
EXPOSED err_t rewire_syscall_hook(reg_t syscall, hook_t entry, hook_t exit);
EXPOSED err_t rewire_syscall_unhook(reg_t syscall);
EXPOSED err_t rewire_syscall_hook_default(hook_t entry, hook_t exit);
EXPOSED err_t rewire_syscall_unhook_default();
EXPOSED err_t rewire_subject_id(pid_t* pid);
EXPOSED err_t rewire_subject_get_regs(user_regs_struct* regs);
EXPOSED err_t rewire_subject_set_regs(const user_regs_struct* regs);
EXPOSED err_t rewire_subject_load_cstr(const char* subject_addr, char* buf, uint32_t* read, uint32_t n);

33
include/wirekit.hpp Normal file
View File

@ -0,0 +1,33 @@
#pragma once
#include <sys/types.h>
#include <sys/user.h>
#include <cstdint>
using reg_t = decltype(user_regs_struct::orig_rax);
using hook_t = void(*)();
using err_t = uint8_t;
constexpr err_t REWIRE_SUCCESS = 0;
constexpr err_t REWIRE_FAILURE = 1;
constexpr err_t REWIRE_FAILURE_NOCTX = 2;
extern "C" {
// Exposed rewire functions - to be used by wirekit
err_t rewire_syscall_hook(reg_t syscall, hook_t entry, hook_t exit);
err_t rewire_syscall_unhook(reg_t syscall); // Alias for rewire_syscall_hook(syscall, nullptr, nullptr)
err_t rewire_syscall_hook_default(hook_t entry, hook_t exit);
err_t rewire_syscall_unhook_default(); // Alias for rewire_syscall_hook_default(nullptr, nullptr);
err_t rewire_subject_id(pid_t* pid);
err_t rewire_subject_get_regs(user_regs_struct* regs);
err_t rewire_subject_set_regs(const user_regs_struct* regs);
err_t rewire_subject_load_cstr(const char* subject_addr, char* buf, uint32_t* read, uint32_t n);
// Wirekit control functions - called by rewire for setup
err_t wirekit_prepare(); // NOCTX - Called when wirekit is loaded
err_t wirekit_command_start(int argc, const char* const* argv); // NOCTX - Called before command is executed
void wirekit_command_exit(); // Optional, NOCTX - Called after command finished executing
void wirekit_subject_start(); // Optional - Called when a new thread starts working on the current command
void wirekit_subject_exit(); // Optional - Called when a thread stops working on the current command
}

182
src/environment.cpp Normal file
View File

@ -0,0 +1,182 @@
#include "environment.hpp"
#include <cstring>
Environment ENV;
// Trace context collection
TraceContextCollection::TraceContextCollection(TraceContextCollection&& other) {
m_contexts = std::move(other.m_contexts);
m_active = other.m_active;
other.context_clear_active();
}
TraceContextCollection& TraceContextCollection::operator=(TraceContextCollection&& other) {
if (this == &other)
return *this;
m_contexts = std::move(other.m_contexts);
m_active = other.m_active;
other.context_clear_active();
return *this;
}
TraceContextCollection::TraceContextCollection() {}
TraceContextCollection::~TraceContextCollection() {}
bool TraceContextCollection::add_context(pid_t pid) {
if (m_contexts.contains(pid))
return false;
std::unique_ptr<TraceContext> new_context = std::make_unique<TraceContext>();
if (!new_context)
return false;
new_context->pid = pid;
new_context->mode = TraceContext::EXECUTION_MODE::STARTING;
std::memset(&new_context->regs, 0, sizeof(new_context->regs));
new_context->regs_dirty = false;
m_contexts.emplace(pid, std::move(new_context));
return true;
}
void TraceContextCollection::delete_context(pid_t pid) {
if (m_contexts.contains(pid))
m_contexts.erase(pid);
if (m_active == pid)
context_clear_active();
}
void TraceContextCollection::clear() {
m_contexts.clear();
context_clear_active();
}
TraceContext* TraceContextCollection::context(pid_t pid) {
if (!m_contexts.contains(pid))
return nullptr;
return m_contexts.at(pid).get();
}
const TraceContext* TraceContextCollection::context(pid_t pid) const {
if (!m_contexts.contains(pid))
return nullptr;
return m_contexts.at(pid).get();
}
void TraceContextCollection::context_set_active(pid_t pid) {
if (m_contexts.contains(pid))
m_active = pid;
else
context_clear_active();
}
void TraceContextCollection::context_clear_active() {
m_active = -1;
}
TraceContext* TraceContextCollection::context_get_active() {
return context(m_active);
}
const TraceContext* TraceContextCollection::context_get_active() const {
return context(m_active);
}
// Environment
Environment::Environment() {
clear();
}
Environment::~Environment() {}
void Environment::register_hooks(reg_t syscall, hook_t entry, hook_t exit) {
if (!entry && !exit) {
if (m_hooks.contains(syscall))
m_hooks.erase(syscall);
return;
}
if (!m_hooks.contains(syscall))
m_hooks.emplace(syscall, HookPair{});
HookPair& hooks = m_hooks.at(syscall);
hooks.entry = entry;
hooks.exit = exit;
}
void Environment::register_default_hooks(hook_t entry, hook_t exit) {
m_default_hooks.entry = entry;
m_default_hooks.exit = exit;
}
bool Environment::entry_hook(reg_t syscall) const {
if (!m_hooks.contains(syscall))
return false;
const HookPair& hooks = m_hooks.at(syscall);
if (!hooks.entry)
return false;
hooks.entry();
return true;
}
bool Environment::exit_hook(reg_t syscall) const {
if (!m_hooks.contains(syscall))
return false;
const HookPair& hooks = m_hooks.at(syscall);
if (!hooks.exit)
return false;
hooks.exit();
return true;
}
bool Environment::default_entry_hook() const {
if (!m_default_hooks.entry)
return false;
m_default_hooks.entry();
return true;
}
bool Environment::default_exit_hook() const {
if (!m_default_hooks.exit)
return false;
m_default_hooks.exit();
return true;
}
TraceContextCollection& Environment::contexts() {
return m_trace_contexts;
}
const TraceContextCollection& Environment::contexts() const {
return m_trace_contexts;
}
void Environment::clear() {
m_hooks.clear();
m_default_hooks.entry = nullptr;
m_default_hooks.exit = nullptr;
m_trace_contexts.clear();
}
// EnvironmentScope to clear Environment on scope exit
EnvironmentScope::EnvironmentScope(Environment& environment) : m_environment{ environment } {}
EnvironmentScope::~EnvironmentScope() {
m_environment.clear();
}

88
src/interface.cpp Normal file
View File

@ -0,0 +1,88 @@
#include "interface.hpp"
#include "environment.hpp"
#include <cstring>
#include <sys/ptrace.h>
#include <errno.h>
err_t(*wirekit_prepare_handle)() = nullptr;
err_t(*wirekit_command_start_handle)(int, const char* const*) = nullptr;
void(*wirekit_command_exit_handle)() = nullptr;
void(*wirekit_subject_start_handle)() = nullptr;
void(*wirekit_subject_exit_handle)() = nullptr;
err_t rewire_syscall_hook(reg_t syscall, hook_t entry, hook_t exit) {
ENV.register_hooks(syscall, entry, exit);
return REWIRE_SUCCESS;
}
err_t rewire_syscall_unhook(reg_t syscall) {
rewire_syscall_hook(syscall, nullptr, nullptr);
return REWIRE_SUCCESS;
}
err_t rewire_syscall_hook_default(hook_t entry, hook_t exit) {
ENV.register_default_hooks(entry, exit);
return REWIRE_SUCCESS;
}
err_t rewire_syscall_unhook_default() {
rewire_syscall_hook_default(nullptr, nullptr);
return REWIRE_SUCCESS;
}
err_t rewire_subject_id(pid_t* pid) {
const TraceContext* ctx = ENV.contexts().context_get_active();
if (!ctx)
return REWIRE_FAILURE_NOCTX;
*pid = ctx->pid;
return REWIRE_SUCCESS;
}
err_t rewire_subject_get_regs(user_regs_struct* regs) {
const TraceContext* ctx = ENV.contexts().context_get_active();
if (!ctx)
return REWIRE_FAILURE_NOCTX;
std::memcpy(regs, &ctx->regs, sizeof(ctx->regs));
return REWIRE_SUCCESS;
}
err_t rewire_subject_set_regs(const user_regs_struct* regs) {
TraceContext* ctx = ENV.contexts().context_get_active();
if (!ctx)
return REWIRE_FAILURE_NOCTX;
std::memcpy(&ctx->regs, regs, sizeof(ctx->regs));
ctx->regs_dirty = true;
return REWIRE_SUCCESS;
}
err_t rewire_subject_load_cstr(const char* subject_addr, char* buf, uint32_t* read, uint32_t n) {
const TraceContext* ctx = ENV.contexts().context_get_active();
if (!ctx)
return REWIRE_FAILURE_NOCTX;
*read = 0;
while (*read < n) {
errno = 0;
long word = ptrace(PTRACE_PEEKDATA, ctx->pid, (char*)subject_addr, NULL); // TODO
if (errno == -1l && errno != 0) // errno differentiates successful -1 word from error
return REWIRE_FAILURE;
for (unsigned i = 0; i < sizeof(long) && *read < n; i++) {
char c = *(reinterpret_cast<char*>(&word) + i); // Unlike bit operations, this is independent of endianness
*(buf++) = c;
(*read)++;
if (c == 0)
return REWIRE_SUCCESS; // Encountered terminating null byte
}
subject_addr += sizeof(long);
}
return REWIRE_SUCCESS;
}

View File

@ -1,9 +1,20 @@
#include <iostream> #include <iostream>
#include <filesystem> #include <filesystem>
#include <vector>
#include <sstream>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include "cli.hpp" #include "cli.hpp"
#include "dl.hpp" #include "dl.hpp"
#include "install.hpp" #include "install.hpp"
#include "interface.hpp"
#include "environment.hpp"
#define FORCEQUIT { std::cout << "Forcefully quitting!" << std::endl; while (true) exit(1); }
int launch_help(int argc, char** argv) { int launch_help(int argc, char** argv) {
std::cout << "Use rewire via one of the following commands:" << std::endl; std::cout << "Use rewire via one of the following commands:" << std::endl;
@ -95,6 +106,200 @@ int launch_list(int argc, char** argv) {
return 0; return 0;
} }
pid_t dispatch_tracee(int argc, const char* const* argv) {
pid_t child = fork();
if (child != 0)
return child; // Either -1 on error or child pid
ptrace(PTRACE_TRACEME, 0, 0, 0);
execvp(argv[0], (char* const*)argv); // Removing const here is in child process
// Note -> Tracee stops with SIGTRAP immediately after execvp!
// Child should never return from execvp
while (true) exit(1);
}
class KillWrapper {
pid_t m_pid;
public:
KillWrapper(const KillWrapper& other) = delete;
KillWrapper(KillWrapper&& other) = delete;
KillWrapper& operator=(const KillWrapper& other) = delete;
KillWrapper& operator=(KillWrapper&& other) = delete;
KillWrapper(pid_t pid) : m_pid{ pid } {}
~KillWrapper() {
if (m_pid != -1)
kill(m_pid, SIGKILL);
}
void release() {
m_pid = -1;
}
};
void run_command(int argc, const char* const* argv) {
EnvironmentScope es { ENV }; // Make sure environment is cleared after each command
// Notify wirekit about command start
if (wirekit_command_start_handle(argc, argv) != REWIRE_SUCCESS) {
std::cout << "Wirekit prohibited command execution!" << std::endl;
return;
}
// Dispatch main command process
pid_t main_pid = dispatch_tracee(argc, argv);
if (main_pid == -1) {
std::cout << "Failed to start command main process!" << std::endl;
return;
}
KillWrapper kw { main_pid }; // Make sure it gets whiped until in main trace loop
int exec_status;
if (waitpid(main_pid, &exec_status, 0) != main_pid || !WIFSTOPPED(exec_status) || WSTOPSIG(exec_status) != SIGTRAP) {
std::cout << "Execvp failed to run the command!" << std::endl;
return;
}
/*
PTRACE_O_EXITKILL -> Kill tracees when tracer exits
PTRACE_O_TRACESYSGOOD -> SIGTRAP signals caused by system call entry/exit stops have 0x80 bit set
PTRACE_O_TRACEFORK, PTRACE_O_TRACEVFORK, PTRACE_O_TRACECLONE -> Automatically trace new children (inherit options and mode)
*/
if (ptrace(PTRACE_SETOPTIONS, main_pid, PTRACE_O_EXITKILL, PTRACE_O_TRACESYSGOOD,
PTRACE_O_TRACEFORK, PTRACE_O_TRACEVFORK, PTRACE_O_TRACECLONE) != 0) {
std::cout << "Tracer failed to set trace options of command main process!" << std::endl;
return;
}
// Let first thread actually start running (until syscall entry)
if (ptrace(PTRACE_SYSCALL, main_pid, 0, 0) != 0) {
std::cout << "Failed to start command main process execution!" << std::endl;
return;
}
// Inform wirekit about first thread and initialize its trace context
ENV.contexts().add_context(main_pid);
ENV.contexts().context(main_pid)->mode = TraceContext::EXECUTION_MODE::USER;
ENV.contexts().context_set_active(main_pid);
if (wirekit_subject_start_handle)
wirekit_subject_start_handle();
ENV.contexts().context_clear_active();
// Start tracer loop
kw.release();
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, __WALL)) != -1 || errno != ECHILD /* No (grand)children left */) {
if (pid == -1) {
std::cout << "Tracer interrupted by signal (errno=" << errno << ")!" << std::endl;
FORCEQUIT;
}
/*
New processes and threads inherit PTRACE_SYSCALL execution mode from parent and thus end up
here when they perform their first syscall. Start logging context at this point.
*/
if (!ENV.contexts().context(pid)) {
ENV.contexts().add_context(pid);
ENV.contexts().context(pid)->mode = TraceContext::EXECUTION_MODE::USER;
ENV.contexts().context_set_active(pid);
if (wirekit_subject_start_handle)
wirekit_subject_start_handle();
ENV.contexts().context_clear_active();
}
// Activate context (definitely exists now)
ENV.contexts().context_set_active(pid);
if (WIFEXITED(status)) {
// A child exited -> notify wirekit and stop tracing it
if (wirekit_subject_exit_handle)
wirekit_subject_exit_handle();
ENV.contexts().delete_context(pid);
} else if (WIFSTOPPED(status)) {
int sig = WSTOPSIG(status);
if (sig == SIGTRAP) {
int event = (unsigned)status >> 16;
if (event == PTRACE_EVENT_FORK || event == PTRACE_EVENT_VFORK || event == PTRACE_EVENT_CLONE) {
; // New thread but nothing to do -> tracer attaches on first syscall
} else {
std::cout << "Received non-syscall SIGTRAP without thread creation event!" << std::endl;
FORCEQUIT;
}
} else if (sig == (SIGTRAP | 0x80) /* 0x80 thanks to PTRACE_O_TRACESYSGOOD */) {
// Tracee was trapped on either syscall entry or exit
TraceContext* ctx = ENV.contexts().context_get_active();
if (ptrace(PTRACE_GETREGS, pid, 0, &ctx->regs) != 0) {
std::cout << "Failed to read tracee registers!" << std::endl;
FORCEQUIT;
}
ctx->regs_dirty = false;
reg_t syscall = ctx->regs.orig_rax;
if (ctx->mode == TraceContext::EXECUTION_MODE::USER)
ENV.entry_hook(syscall) || ENV.default_entry_hook(); // Short-circuit guaranteed by C++ standard ;)
else if (ctx->mode == TraceContext::EXECUTION_MODE::KERNEL)
ENV.exit_hook(syscall) || ENV.default_exit_hook();
if (ctx->regs_dirty) {
if (ptrace(PTRACE_SETREGS, pid, NULL, &ctx->regs) != 0) {
std::cout << "Failed to write tracee registers!" << std::endl;
FORCEQUIT;
}
}
ctx->regs_dirty = false;
auto progress_mode = [](TraceContext::EXECUTION_MODE m) {
switch (m) {
case TraceContext::EXECUTION_MODE::STARTING:
return TraceContext::EXECUTION_MODE::USER;
case TraceContext::EXECUTION_MODE::USER:
return TraceContext::EXECUTION_MODE::KERNEL;
case TraceContext::EXECUTION_MODE::KERNEL:
return TraceContext::EXECUTION_MODE::USER;
}
return TraceContext::EXECUTION_MODE::STARTING;
};
ctx->mode = progress_mode(ctx->mode);
}
// Let tracee continue until next system call event (entry or exit)
if (ptrace(PTRACE_SYSCALL, pid, 0, 0) != 0) {
std::cout << "Failed to continue command execution after SIGTRAP!" << std::endl;
FORCEQUIT;
}
} // else { Weird signal is happening, should be irrelevant to wirekit though! }
// Deactivate current context
ENV.contexts().context_clear_active();
}
// Notify wirekit that command has finished executing (no threads are left)
if (wirekit_command_exit_handle)
wirekit_command_exit_handle();
}
int launch_run(int argc, char** argv) { int launch_run(int argc, char** argv) {
if (argc < 2) { if (argc < 2) {
std::cout << "Invalid usage, try \"rewire help\" for more information!" << std::endl; std::cout << "Invalid usage, try \"rewire help\" for more information!" << std::endl;
@ -119,16 +324,64 @@ int launch_run(int argc, char** argv) {
return 1; return 1;
} }
/* // Collect wirekit control function handles
TODO if (!(wirekit_prepare_handle = (err_t(*)())wirekit.resolve("wirekit_prepare"))) {
environment.hpp/cpp std::cout << "Wirekit is missing \"wirekit_prepare\" implementation!" << std::endl;
class Environment -> kit handles, hooks, trace contexts return 1;
class CommandScope -> hooks and trace contexts can only be added while scope holds environment, all cleared on scope exit }
class TraceContext -> execution data on individual tracee threads
class TraceContextCollection -> management of all trace contexts and selection of active context
*/
// TODO -> Execute single command (argc > 2) or shell if (!(wirekit_command_start_handle = (err_t(*)(int, const char* const*))wirekit.resolve("wirekit_command_start"))) {
std::cout << "Wirekit is missing \"wirekit_command_start\" implementation!" << std::endl;
return 1;
}
wirekit_command_exit_handle = (void(*)())wirekit.resolve("wirekit_command_exit");
wirekit_subject_start_handle = (void(*)())wirekit.resolve("wirekit_subject_start");
wirekit_subject_exit_handle = (void(*)())wirekit.resolve("wirekit_subject_exit");
// Prepare wirekit
err_t prepare_err;
if ((prepare_err = wirekit_prepare_handle()) != REWIRE_SUCCESS) {
std::cout << "Wirekit preparation failed (wirekit_prepare returned " << prepare_err << ")!" << std::endl;
return 1;
}
// Execute commands in rewired syscall environment
if (argc > 2) {
run_command(argc - 2, (const char**)(argv + 2)); // Execute a single command
} else {
// Shell mode
while (true) {
std::cout << "> ";
std::string command;
std::getline(std::cin, command);
// Split command at whitespaces
std::stringstream command_stream { command };
std::vector<std::string> command_split; // No need to reserve, presumably not many elements
std::string part;
while (command_stream >> std::quoted(part))
command_split.emplace_back(std::move(part));
if (command_split.empty())
continue;
if (command_split[0] == "exit")
break;
// Compile argv (cstr pointer array)
std::vector<const char*> command_split_cstr;
command_split_cstr.resize(command_split.size() + 1, nullptr); // +1 for nullptr termination
for (std::size_t i = 0; i < command_split.size(); i++)
command_split_cstr[i] = command_split[i].c_str();
// Execute command
run_command((int)command_split_cstr.size() - 1, command_split_cstr.data());
}
}
return 0; return 0;
} }