diff options
| author | srdusr <[email protected]> | 2024-05-17 19:54:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-05-17 19:54:00 +0200 |
| commit | e0f4c701028aa81026a17cf9ebfb36112184f4bc (patch) | |
| tree | 31c05e4ccbba0dd2ab4c0567630275ebfc6cd264 /include/wireframe/privileges.hpp | |
| parent | 08332a4195956611db80a2cfe3710d760cbd6acf (diff) | |
| download | packeteer-e0f4c701028aa81026a17cf9ebfb36112184f4bc.tar.gz packeteer-e0f4c701028aa81026a17cf9ebfb36112184f4bc.zip | |
Add privilege dropping, AF_PACKET demo, ICMP, checksum validation, --help, and TCP reassembly
Rounds out the build order in PLAN.md with six incremental additions:
drop root privileges immediately after opening the capture handle;
a standalone AF_PACKET/mmap ring-buffer demo (kept separate from
CaptureSession, see its header comment for why); ICMPv4/ICMPv6 type
and code decoding; opt-in IPv4/TCP/UDP checksum validation (-c);
CLI --help; and opt-in, in-order-only TCP stream reassembly (-a) so
HTTP requests/responses split across segments can be seen whole.
Each addition is unit-tested and, where it touches live traffic
behavior, verified against real captured packets - see PLAN.md's
Decisions section for the verification notes on each.
Diffstat (limited to 'include/wireframe/privileges.hpp')
| -rw-r--r-- | include/wireframe/privileges.hpp | 87 |
1 files changed, 87 insertions, 0 deletions
diff --git a/include/wireframe/privileges.hpp b/include/wireframe/privileges.hpp new file mode 100644 index 0000000..69df725 --- /dev/null +++ b/include/wireframe/privileges.hpp @@ -0,0 +1,87 @@ +#pragma once + +#ifndef _WIN32 +#include <grp.h> +#include <unistd.h> +#endif + +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <optional> +#include <string> + +// After pcap_open_live() succeeds, the process has gotten everything +// CAP_NET_RAW exists for - running the rest of the program (decoding +// untrusted packet bytes, an interactive TUI/GUI event loop) as root +// from that point on is unnecessary exposure, and PLAN.md says as much +// directly: "Drop privileges immediately after opening the capture +// handle; use CAP_NET_RAW via file capabilities instead of running as +// root." +// +// The recommended path doesn't need this file at all: run +// `sudo setcap cap_net_raw+ep <binary>` once, then invoke the binary +// directly, unprivileged, forever after - CAP_NET_RAW alone is enough +// for pcap_open_live(), no root required at any point. This exists for +// the case someone still runs the binary via sudo (out of habit, or +// because setcap isn't available/permitted in some environments): drop +// straight back to the invoking user immediately, so the rest of the +// process's lifetime - including any -w output file, which then ends +// up owned by that user instead of root - runs unprivileged either way. +namespace wireframe { + +// Drops from root to the user who actually invoked the program, via +// sudo's SUDO_UID/SUDO_GID (which sudo always sets). A no-op if not +// currently root, or if SUDO_UID isn't set (e.g. a genuine root login, +// not sudo - there's no "real" user to drop to in that case). +// +// setuid() to a nonzero UID also clears the process's Linux capability +// sets as a kernel-level side effect, so this covers both "running as +// root via sudo" and "root's own CAP_NET_RAW" the same way, without a +// separate libcap dependency. +// +// Returns an error message on failure. The drop is safety-critical: a +// failure here should be treated as fatal by the caller, not silently +// ignored while the process keeps running as root. +inline std::optional<std::string> drop_privileges_if_root() { +#ifdef _WIN32 + return std::nullopt; // no POSIX privilege model to drop from +#else + if (geteuid() != 0) return std::nullopt; // already unprivileged + + const char* sudo_uid = std::getenv("SUDO_UID"); + const char* sudo_gid = std::getenv("SUDO_GID"); + if (sudo_uid == nullptr || sudo_gid == nullptr) { + return std::nullopt; // no safe target to drop to + } + + uid_t target_uid = static_cast<uid_t>(std::strtoul(sudo_uid, nullptr, 10)); + gid_t target_gid = static_cast<gid_t>(std::strtoul(sudo_gid, nullptr, 10)); + + // Order matters: groups and GID need root to change, so they must + // be dropped before UID - once UID is gone, so is the privilege + // to change the others. + if (setgroups(1, &target_gid) == -1) { + return "setgroups failed: " + std::string(std::strerror(errno)); + } + if (setgid(target_gid) == -1) { + return "setgid failed: " + std::string(std::strerror(errno)); + } + if (setuid(target_uid) == -1) { + return "setuid failed: " + std::string(std::strerror(errno)); + } + + // Defense in depth (standard advice from setuid-privilege-drop + // write-ups): confirm root can't be reclaimed. If the saved-UID + // was somehow left at 0, this would succeed and silently undo the + // drop - so a *successful* setuid(0) here means something is + // wrong, and is treated as the failure case. + if (setuid(0) != -1) { + return "failed to permanently drop root (setuid(0) unexpectedly succeeded)"; + } + + return std::nullopt; +#endif +} + +} // namespace wireframe |