Skip to content

Instantly share code, notes, and snippets.

@snarkmaster
Created October 3, 2020 03:11
Show Gist options
  • Save snarkmaster/4d019ab39e9adbe4fd7269f4f9812692 to your computer and use it in GitHub Desktop.
Save snarkmaster/4d019ab39e9adbe4fd7269f4f9812692 to your computer and use it in GitHub Desktop.
/*
This is a unfinished program that's part of a `libcap-ng` bug report.
The bug repro looks like this -- the target PID 3554846 is just a random
unprivileged process:
echo target: &&
grep ^Cap /proc/3554846/status &&
sudo capsh --drop=cap_chown -- -c '
echo under capsh: &&
/usr/bin/grep ^Cap /proc/self/status &&
./clonecaps 3554846 -- \
/usr/bin/grep ^Cap /proc/self/status'
The output looks like this (extra newlines for readability):
target:
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
under capsh:
CapInh: 0000000000000000
CapPrm: 0000003ffffffffe
CapEff: 0000003ffffffffe
CapBnd: 0000003ffffffffe
CapAmb: 0000000000000000
XXX procfs CapInh: 0000000000000000
XXX procfs CapPrm: 0000003ffffffffe
XXX procfs CapEff: 0000003ffffffffe
XXX procfs CapBnd: 0000003ffffffffe
XXX procfs CapAmb: 0000000000000000
XXX getpid() initial --> i 0, p 3ffffffffe, e 3ffffffffe, bs 3ffffffffe, a 0
XXX procfs CapInh: 0000000000000000
XXX procfs CapPrm: 0000000000000000
XXX procfs CapEff: 0000000000000000
XXX procfs CapBnd: 0000003fffffffff
XXX procfs CapAmb: 0000000000000000
XXX target PID --> i 0, p 0, e 0, bs 3fffffffff, a 0
XXX check 4 0 0
XXX check 2 0 0
XXX check 1 0 0
XXX check 8 3fffffffff 3fffffffff
XXX check 16 0 0
XXX procfs CapInh: 0000000000000000
XXX procfs CapPrm: 0000000000000000
XXX procfs CapEff: 0000000000000000
XXX procfs CapBnd: 0000003ffffffffe
XXX procfs CapAmb: 0000000000000000
XXX getpid() final --> i 0, p 0, e 0, bs 3ffffffffe, a 0
CapInh: 0000000000000000
CapPrm: 0000003ffffffffe
CapEff: 0000003ffffffffe
CapBnd: 0000003ffffffffe
CapAmb: 0000000000000000
The `libcap-ng` bug is that the `capng_have_capability()` values shown as
`check` report full capabilities (ending in `fff`, even though we dropped
`CAP_CHOWN` via`capsh`, and thus the actual bounding set as read from
`procfs` (preceding "getpid() final") ended in `ffe`.
Besides the bug in `libcap-ng`, this has at least one bug (not handling
long `status` lines), and many debug prints.
The final variant will be part of the eventual "antlir" OSS release.
*/
#include <cap-ng.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// Returns the number of the last valid capability for the current kernel,
// or -1 on error.
int find_last_cap() {
int last_cap = -1; // Negative so we fail if `sscanf` fails
FILE* last_cap_file = fopen("/proc/sys/kernel/cap_last_cap", "re");
if (last_cap_file == NULL) {
perror("open /proc/sys/kernel/cap_last_cap");
return -1;
}
// No error-checking since `last_cap` will stay at -1 on failure to match.
fscanf(last_cap_file, "%d", &last_cap);
fclose(last_cap_file);
if (last_cap < 0 || last_cap >= 64) {
fprintf(stderr, "Got %d in /proc/sys/kernel/cap_last_cap\n", last_cap);
return -1;
}
return last_cap;
}
// Returns 1 if all `bits` were added successfully, 0 otherwise
int add_all_caps(int last_cap, capng_type_t cap_type, __u64 bits) {
for (int cap = 0; cap <= last_cap; ++cap) {
if (bits & ((__u64)1 << cap)) {
if (capng_update(CAPNG_ADD, cap_type, cap) != 0) {
fprintf(
stderr,
"Failed to add capability %d of capability type %d\n",
cap, cap_type
);
return 0;
}
}
}
return 1;
}
// Returns 1 if `expected_bits` matches the actual setting, 0 otherwise
int check_all_caps(int last_cap, capng_type_t cap_type, __u64 expected_bits) {
__u64 actual_bits = 0;
for (int cap = 0; cap <= last_cap; ++cap) {
actual_bits |= (__u64)capng_have_capability(cap_type, cap) << cap;
}
fprintf(
stderr,
"XXX check %d %llx %llx\n",
cap_type, actual_bits, expected_bits
);
if (actual_bits != expected_bits) {
fprintf(
stderr,
"Unexpected caps of type %d: actual %llx != expected %llx\n",
cap_type, actual_bits, expected_bits
);
return 0;
}
return 1;
}
// Detail for parsing `/proc/PID/status`.
int _match(
char* buf, const char* pref, int* cap_type, int match_type, int* pref_len
) {
*pref_len = strlen(pref);
if (strncmp(buf, pref, *pref_len) == 0) {
*cap_type = match_type;
return 1;
}
return 0;
}
typedef struct {
__u64 inheritable;
__u64 permitted;
__u64 effective;
__u64 bounding_set;
__u64 ambient;
} cap_bits_t;
// Parses the `Cap...:` lines from `/proc/pid/status` and populates `cap_bits`.
// Returns 1 on success. On error, logs to stderr and returns 0.
int read_cap_bits_for_pid(pid_t pid, cap_bits_t* cap_bits) {
memset(cap_bits, 0, sizeof(*cap_bits));
// We'll compare these to make sure we saw all the expected procfs lines.
int expected_cap_types =
CAPNG_INHERITABLE | CAPNG_PERMITTED | CAPNG_EFFECTIVE
| CAPNG_BOUNDING_SET | CAPNG_AMBIENT;
int actual_cap_types = 0;
// `/proc/PID/status` would fit even with 64-bit PIDs
char status_filename[64];
if (snprintf(
status_filename, sizeof(status_filename), "/proc/%d/status", pid
) >= sizeof(status_filename)) {
fprintf(stderr, "PID too long??? %d\n", pid);
return 1;
}
FILE* status_file = fopen(status_filename, "re");
if (status_file == NULL) {
perror("open /proc/PID/status");
return 0;
}
// Not all lines are under 64 bytes (the max length is ~unbounded thanks
// to groups), but Cap lines are.
char buf[256]; // XXX 64
while (fgets(buf, sizeof(buf), status_file)) {
// Both values are populated by `match`
int cap_type = 0;
int pref_len = 0;
if (!(
_match(buf, "CapInh:\t", &cap_type, CAPNG_INHERITABLE, &pref_len)
|| _match(buf, "CapPrm:\t", &cap_type, CAPNG_PERMITTED, &pref_len)
|| _match(buf, "CapEff:\t", &cap_type, CAPNG_EFFECTIVE, &pref_len)
|| _match(buf, "CapBnd:\t", &cap_type, CAPNG_BOUNDING_SET, &pref_len)
|| _match(buf, "CapAmb:\t", &cap_type, CAPNG_AMBIENT, &pref_len)
)) {
// XXX for now can just crank up to 256, then
// if not newline-terminated XXX read until newline or eof, then skip
continue;
}
fprintf(stderr, "XXX procfs %s", buf);
// Fail on duplicate cap types in the input
if (actual_cap_types & cap_type) {
fprintf(
stderr,
"%s: Capability type %d occurred more than once\n",
status_filename, cap_type
);
return 0;
}
actual_cap_types |= cap_type;
// Read out the bits for this capability, we'll apply them later
char* end_of_bits = NULL;
__u64 bits = strtoull(buf + pref_len, &end_of_bits, 16);
// We should have read 16 hex bytes, terminated by a newline.
if ((end_of_bits - (buf + pref_len)) != 16 || end_of_bits[0] != '\n') {
fprintf(
stderr,
"%s: Failed to parse value %s for capability type %d\n",
status_filename, buf + pref_len, cap_type
);
return 0;
}
if (cap_type == CAPNG_INHERITABLE) {
cap_bits->inheritable = bits;
} else if (cap_type == CAPNG_PERMITTED) {
cap_bits->permitted = bits;
} else if (cap_type == CAPNG_EFFECTIVE) {
cap_bits->effective = bits;
} else if (cap_type == CAPNG_BOUNDING_SET) {
cap_bits->bounding_set = bits;
} else if (cap_type == CAPNG_AMBIENT) {
cap_bits->ambient = bits;
}
}
fclose(status_file);
if (actual_cap_types != expected_cap_types) {
fprintf(
stderr,
"%s: Missing capability types: %d vs %d\n",
status_filename, actual_cap_types, expected_cap_types
);
return 0;
}
return 1;
}
void XXX_fprint_cap_bits(FILE* file, const char* msg, cap_bits_t cap_bits) {
fprintf(
file,
"XXX %s --> i %llx, p %llx, e %llx, bs %llx, a %llx\n",
msg,
cap_bits.inheritable,
cap_bits.permitted,
cap_bits.effective,
cap_bits.bounding_set,
cap_bits.ambient
);
}
int main(int argc, char** argv) {
if (argc < 3 || strcmp("--", argv[2]) != 0) {
fprintf(stderr, "Usage: clonecaps <PID> -- /path/to/prog argv1 ...\n");
return 1;
}
char* pid_arg = argv[1];
argv += 3; // Skip "our" args, this is now ready to `execv`.
argc -= 3;
// Parse the target PID command-line argument.
char* endptr = pid_arg;
pid_t pid = strtol(pid_arg, &endptr, 10);
if (pid_arg[0] == 0 || endptr[0] != 0) {
fprintf(stderr, "Invalid PID: %s\n", pid_arg);
return 1;
}
// The running kernel may not match our compile-time headers.
int last_cap = find_last_cap();
if (last_cap == -1) { // The function already printed the error
return 1;
}
cap_bits_t cap_bits;
if (!read_cap_bits_for_pid(getpid(), &cap_bits)) { // XXX delme
return 1; // An error was already printed
}
XXX_fprint_cap_bits(stderr, "getpid() initial", cap_bits);
if (!read_cap_bits_for_pid(pid, &cap_bits)) {
return 1; // An error was already printed
}
XXX_fprint_cap_bits(stderr, "target PID", cap_bits);
capng_clear(CAPNG_SELECT_ALL); // Clear traditional, bounding, ambient
// Clone the target's values
if (!(
add_all_caps(last_cap, CAPNG_INHERITABLE, cap_bits.inheritable)
&& add_all_caps(last_cap, CAPNG_PERMITTED, cap_bits.permitted)
&& add_all_caps(last_cap, CAPNG_EFFECTIVE, cap_bits.effective)
&& add_all_caps(last_cap, CAPNG_BOUNDING_SET, cap_bits.bounding_set)
&& add_all_caps(last_cap, CAPNG_AMBIENT, cap_bits.ambient)
)) {
return 1; // `add_all_caps` already printed an error
}
// Apply traditional & bounding
if (capng_apply(CAPNG_SELECT_ALL) != 0) {
fprintf(stderr, "Failed to apply new capabilities\n");
return 1;
}
// Due to the following bug, ambient capabilities only get applied the
// second time around: https://github.com/stevegrubb/libcap-ng/issues/18
if (capng_apply(CAPNG_SELECT_ALL) != 0) {
fprintf(stderr, "Failed to apply new capabilities a second time\n");
return 1;
}
if (!(
check_all_caps(last_cap, CAPNG_INHERITABLE, cap_bits.inheritable)
&& check_all_caps(last_cap, CAPNG_PERMITTED, cap_bits.permitted)
&& check_all_caps(last_cap, CAPNG_EFFECTIVE, cap_bits.effective)
&& check_all_caps(last_cap, CAPNG_BOUNDING_SET, cap_bits.bounding_set)
&& check_all_caps(last_cap, CAPNG_AMBIENT, cap_bits.ambient)
)) {
return 1; // `check_all_caps` already printed an error
}
if (!read_cap_bits_for_pid(getpid(), &cap_bits)) { // XXX delme
return 1; // An error was already printed
}
XXX_fprint_cap_bits(stderr, "getpid() final", cap_bits);
execv(argv[0], argv);
perror("execv");
return 1;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment