From 7763f5aa67bd7ca29be1417293d403ebba6128eb Mon Sep 17 00:00:00 2001 From: GlitchedCode922 Date: Sat, 10 Jan 2026 17:26:59 +0200 Subject: [PATCH] Add configurable binfmt_misc support --- src/GNUmakefile | 1 + src/cli/proot.c | 6 + src/cli/proot.h | 18 +++ src/execve/binfmt.c | 336 +++++++++++++++++++++++++++++++++++++++++++ src/execve/binfmt.h | 25 ++++ src/execve/enter.c | 36 ++++- src/execve/shebang.c | 110 ++++++-------- 7 files changed, 463 insertions(+), 69 deletions(-) create mode 100644 src/execve/binfmt.c create mode 100644 src/execve/binfmt.h diff --git a/src/GNUmakefile b/src/GNUmakefile index 96035576..e46e856a 100644 --- a/src/GNUmakefile +++ b/src/GNUmakefile @@ -25,6 +25,7 @@ OBJECTS += \ execve/enter.o \ execve/exit.o \ execve/shebang.o \ + execve/binfmt.o \ execve/elf.o \ execve/ldso.o \ execve/auxv.o \ diff --git a/src/cli/proot.c b/src/cli/proot.c index 29365e81..a97aa64e 100644 --- a/src/cli/proot.c +++ b/src/cli/proot.c @@ -20,6 +20,7 @@ * 02110-1301 USA. */ +#include #include /* str*(3), */ #include /* assert(3), */ #include /* printf(3), fflush(3), */ @@ -335,6 +336,11 @@ static int handle_option_p(Tracee *tracee, const Cli *cli UNUSED, const char *va return 0; } +static int handle_option_binfmt_rules(Tracee *tracee UNUSED, const struct Cli *cli UNUSED, const char *value) { + atexit(clear_binfmt_rule_list); + return read_binfmt_rules_from_file(value); +} + /** * Initialize @tracee->qemu. */ diff --git a/src/cli/proot.h b/src/cli/proot.h index 3f619313..636f3c37 100644 --- a/src/cli/proot.h +++ b/src/cli/proot.h @@ -4,6 +4,7 @@ #define PROOT_CLI_H #include "cli/cli.h" +#include "execve/binfmt.h" #ifndef VERSION #define VERSION "5.1.0" @@ -67,6 +68,7 @@ static int handle_option_kill_on_exit(Tracee *tracee, const Cli *cli, const char static int handle_option_L(Tracee *tracee, const Cli *cli, const char *value); static int handle_option_H(Tracee *tracee, const Cli *cli, const char *value); static int handle_option_p(Tracee *tracee, const Cli *cli, const char *value); +static int handle_option_binfmt_rules(Tracee *tracee, const struct Cli *cli, const char *value); static int pre_initialize_bindings(Tracee *, const Cli *, size_t, char *const *, size_t); static int post_initialize_exe(Tracee *, const Cli *, size_t, char *const *, size_t); @@ -284,6 +286,22 @@ Copyright (C) 2015 STMicroelectronics, licensed under GPL v2 or later.", .description = "Correct the size returned from lstat for symbolic links.", .detail = "", }, + { + .class = "Extension options", + .arguments = { + { .name = "--binfmt-rules", .separator = ' ', .value = "path" }, + { .name = NULL, .separator = '\0', .value = NULL } + }, + .handler = handle_option_binfmt_rules, + .description = "Load binfmt_misc rules from the specified file.", + .detail = "\tThis option allows loading binfmt_misc rules from a file.\n\ +\tEach rule in the file should follow this format:\n\ +\n\ +\t:name:type:offset:magic:mask:interpreter:\n\ +\n\ +\tThis is useful for adding support for executing foreign\n\ +\tbinary formats within the proot environment.", + }, { .class = "Alias options", .arguments = { { .name = "-R", .separator = ' ', .value = "path" }, diff --git a/src/execve/binfmt.c b/src/execve/binfmt.c new file mode 100644 index 00000000..0da7b144 --- /dev/null +++ b/src/execve/binfmt.c @@ -0,0 +1,336 @@ +#include "execve/binfmt.h" +#include "execve/execve.h" +#include "execve/aoxp.h" +#include "tracee/reg.h" +#include "tracee/tracee.h" +#include "cli/note.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BinfmtRule* rules = NULL; +int rules_number = 0; + +int register_binfmt(const BinfmtRule* rule) { + if (!rules) { + rules = malloc(sizeof(BinfmtRule)); + if (!rules) { + return -ENOMEM; + } + } else { + BinfmtRule* new_rules = realloc(rules, sizeof(BinfmtRule) * (rules_number + 1)); + if (!new_rules) { + return -ENOMEM; + } + rules = new_rules; + } + BinfmtRule r = *rule; + // Ensure mask is applied to magic + if (r.type == 'M') { + for (size_t i = 0; i < PATH_MAX; i++) { + r.magic[i] &= r.mask[i]; + } + } + // Add the new rule to the array + rules[rules_number++] = r; + return 0; +} + +int unregister_binfmt(const char *name) { + if (!rules || rules_number == 0) { + return -ENOENT; + } + for (int i = 0; i < rules_number; i++) { + if (strcmp(rules[i].name, name) == 0) { + // Found the rule, remove it + for (int j = i; j < rules_number - 1; j++) { + rules[j] = rules[j + 1]; + } + rules_number--; + return 0; + } + } + return -ENOENT; +} + +int read_binfmt_rules_from_file(const char *filepath) { + FILE *file = fopen(filepath, "r"); + if (!file) { + note(NULL, ERROR, SYSTEM, "Failed to open binfmt configuration file"); + return -1; + } + // Read file line by line + char line[4096]; + while (fgets(line, sizeof(line), file)) { + line[strcspn(line, "\n")] = 0; // Remove newline + BinfmtRule rule; + memset(&rule, 0, sizeof(BinfmtRule)); + // Parse line (:name:type:offset:magic:mask:interpreter:) + int l = 0; + if (sscanf(line, ":%255[^:]:%c:%zu:%255[^:]:%255[^:]:%255[^:]:%n", rule.name, &rule.type, &rule.offset, rule.magic, rule.mask, rule.interpreter, &l) != 6 || line[l] != '\0') { + note(NULL, ERROR, INTERNAL, "Failed to parse binfmt configuration line: %s", line); + return -1; + } + // Run escape sequences in magic, mask, and interpreter + int magic_len = 0; + int mask_len = 0; + int interp_len = 0; + char *ptr = rule.magic; + char *dst = rule.magic; + while (*ptr) { + if (*ptr == '\\') { + ptr++; + if (*ptr == 'n') { + *dst++ = '\n'; + magic_len++; + } else if (*ptr == 't') { + *dst++ = '\t'; + magic_len++; + } else if (*ptr == 'r') { + *dst++ = '\r'; + magic_len++; + } else if (*ptr == '\\') { + *dst++ = '\\'; + magic_len++; + } else if (*ptr == 'x') { + ptr++; + char hex[3] = {0}; + if (isxdigit(ptr[0])) hex[0] = *ptr++; + if (isxdigit(ptr[0])) hex[1] = *ptr++; + *dst++ = (char)strtol(hex, NULL, 16); + magic_len++; + continue; // skip ptr++ at end of loop because we've already advanced + } else { + *dst++ = *ptr; + magic_len++; + } + } else { + *dst++ = *ptr; + magic_len++; + } + ptr++; + } + // Clean up the rest of the buffer + memset(dst, 0, PATH_MAX - magic_len); + ptr = rule.mask; + dst = rule.mask; + while (*ptr) { + if (*ptr == '\\') { + ptr++; + if (*ptr == 'n') { + *dst++ = '\n'; + mask_len++; + } else if (*ptr == 't') { + *dst++ = '\t'; + mask_len++; + } else if (*ptr == 'r') { + *dst++ = '\r'; + mask_len++; + } else if (*ptr == '\\') { + *dst++ = '\\'; + mask_len++; + } else if (*ptr == 'x') { + ptr++; + char hex[3] = {0}; + if (isxdigit(ptr[0])) hex[0] = *ptr++; + if (isxdigit(ptr[0])) hex[1] = *ptr++; + *dst++ = (char)strtol(hex, NULL, 16); + mask_len++; + continue; // skip ptr++ at end of loop because we've already advanced + } else { + *dst++ = *ptr; + mask_len++; + } + } else { + *dst++ = *ptr; + mask_len++; + } + ptr++; + } + // Clean up the rest of the buffer + memset(dst, 0, PATH_MAX - mask_len); + ptr = rule.interpreter; + dst = rule.interpreter; + while (*ptr) { + if (*ptr == '\\') { + ptr++; + if (*ptr == 'n') { + *dst++ = '\n'; + interp_len++; + } else if (*ptr == 't') { + *dst++ = '\t'; + interp_len++; + } else if (*ptr == 'r') { + *dst++ = '\r'; + interp_len++; + } else if (*ptr == '\\') { + *dst++ = '\\'; + interp_len++; + } else if (*ptr == 'x') { + ptr++; + char hex[3] = {0}; + if (isxdigit(ptr[0])) hex[0] = *ptr++; + if (isxdigit(ptr[0])) hex[1] = *ptr++; + *dst++ = (char)strtol(hex, NULL, 16); + interp_len++; + continue; // skip ptr++ at end of loop because we've already advanced + } else { + *dst++ = *ptr; + interp_len++; + } + } else { + *dst++ = *ptr; + interp_len++; + } + ptr++; + } + // Clean up the rest of the buffer + memset(dst, 0, PATH_MAX - interp_len); + + // Register rule + int status = register_binfmt(&rule); + if (status < 0) { + note(NULL, ERROR, INTERNAL, "Failed to register binfmt rule: %s", strerror(-status)); + return -1; + } + } + fclose(file); + return 0; +} + +void clear_binfmt_rule_list() { + free(rules); + rules = NULL; + rules_number = 0; +} + +int check_binfmt(Tracee *tracee, char host_path[PATH_MAX], char user_path[PATH_MAX]) { + ArrayOfXPointers *argv = NULL; + bool has_matched = false; + BinfmtRule* r = NULL; + int status = 0; + char* old_user_path = NULL; + + old_user_path = talloc_strdup(tracee->ctx, user_path); + if (!old_user_path) { + return -ENOMEM; + } + for (int i = 0; i < rules_number; i++) { + r = &rules[i]; + if (r->type == 'E') { + size_t path_len = strlen(user_path); + size_t ext_len = strlen(r->magic); + + if (ext_len > path_len) continue; + if (strcmp(user_path + path_len - ext_len, r->magic) == 0) { + has_matched = true; + break; + } + } + if (r->type == 'M') { + // Find last 1 in the mask + size_t mask_size = 0; + for (int k = PATH_MAX - 1; k >= 0; k--) { + if (r->mask[k] != 0) { + mask_size = k + 1; + break; + } + } + if (mask_size == 0) continue; + // Read magic number from file + char* magic = talloc_array(tracee->ctx, char, mask_size); + if (!magic) { + talloc_free(old_user_path); + return -ENOMEM; + } + int fd = open(host_path, O_RDONLY); + if (fd < 0) { + talloc_free(old_user_path); + talloc_free(magic); + return -errno; + } + if (lseek(fd, r->offset, SEEK_SET) < 0) { + close(fd); + talloc_free(old_user_path); + talloc_free(magic); + return -errno; + } + ssize_t rr = read(fd, magic, mask_size); + if (rr < 0) { + close(fd); + talloc_free(old_user_path); + talloc_free(magic); + return -errno; + } + if ((size_t)rr < mask_size) { + /* Not enough bytes in file */ + talloc_free(magic); + continue; + } + close(fd); + // Apply mask and compare + for (size_t j = 0; j < mask_size; j++) { + magic[j] &= r->mask[j]; + } + if (memcmp(magic, r->magic, mask_size) == 0) { + has_matched = true; + talloc_free(magic); + break; + } + talloc_free(magic); + } + } + + if (has_matched) { + // Rule matched, modify path and arguments + + // Note: The interpreter path is not tokenized; + // the entire string is used as the interpreter path. + strcpy(user_path, r->interpreter); + status = translate_and_check_exec(tracee, host_path, user_path); + if (status < 0) { + talloc_free(old_user_path); + return status; + } + // Fetch argv[] only on demand. + if (argv == NULL) { + status = fetch_array_of_xpointers(tracee, &argv, SYSARG_2, 0); + if (status < 0) { + talloc_free(old_user_path); + return status; + } + } + // Add interpreter to argv + status = resize_array_of_xpointers(argv, 0, 1 + (argv->length == 1)); + if (status < 0) { + talloc_free(old_user_path); + return status; + } + status = write_xpointees(argv, 0, 2, user_path, old_user_path); + if (status < 0) { + talloc_free(old_user_path); + return status; + } + + // Push args + status = push_array_of_xpointers(argv, SYSARG_2); + if (status < 0) { + talloc_free(old_user_path); + return status; + } + + talloc_free(old_user_path); + return 1; + } + + talloc_free(old_user_path); + return 0; +} diff --git a/src/execve/binfmt.h b/src/execve/binfmt.h new file mode 100644 index 00000000..05c8829f --- /dev/null +++ b/src/execve/binfmt.h @@ -0,0 +1,25 @@ +#ifndef BINFMT_H +#define BINFMT_H + +#include +#include + +#include "tracee/tracee.h" + +typedef struct { + char name[256]; + char type; // M for magic number, E for extension + size_t offset; + char magic[PATH_MAX]; + char mask[PATH_MAX]; + char interpreter[PATH_MAX]; +} BinfmtRule; + +int register_binfmt(const BinfmtRule* rule); +int unregister_binfmt(const char* name); +int read_binfmt_rules_from_file(const char* filepath); +void clear_binfmt_rule_list(); + +int check_binfmt(Tracee* tracee, char host_path[PATH_MAX], char user_path[PATH_MAX]); + +#endif diff --git a/src/execve/enter.c b/src/execve/enter.c index c562d14a..73552479 100644 --- a/src/execve/enter.c +++ b/src/execve/enter.c @@ -26,6 +26,7 @@ #include /* E*, */ #include /* assert(3), */ #include /* talloc*, */ +#include /* MAXSYMLINKS, */ #include /* PROT_*, */ #include /* strlen(3), strcpy(3), */ #include /* getenv(3), */ @@ -34,6 +35,7 @@ #include "execve/execve.h" #include "execve/shebang.h" +#include "execve/binfmt.h" #include "execve/aoxp.h" #include "execve/ldso.h" #include "execve/elf.h" @@ -332,7 +334,7 @@ static void add_load_base(LoadInfo *load_info, word_t load_base) static void compute_load_addresses(Tracee *tracee) { if (IS_POSITION_INDENPENDANT(tracee->load_info->elf_header) - && tracee->load_info->mappings[0].addr == 0) { + && tracee->load_info->mappings[0].addr == 0) { #if defined(HAS_LOADER_32BIT) if (IS_CLASS32(tracee->load_info->elf_header)) add_load_base(tracee->load_info, EXEC_PIC_ADDRESS_32); @@ -346,7 +348,7 @@ static void compute_load_addresses(Tracee *tracee) return; if (IS_POSITION_INDENPENDANT(tracee->load_info->interp->elf_header) - && tracee->load_info->interp->mappings[0].addr == 0) { + && tracee->load_info->interp->mappings[0].addr == 0) { #if defined(HAS_LOADER_32BIT) if (IS_CLASS32(tracee->load_info->elf_header)) add_load_base(tracee->load_info->interp, INTERP_PIC_ADDRESS_32); @@ -622,15 +624,37 @@ int translate_execve_enter(Tracee *tracee) if (raw_path == NULL) return -ENOMEM; - status = expand_shebang(tracee, host_path, user_path); + status = translate_and_check_exec(tracee, host_path, user_path); if (status < 0) /* The Linux kernel actually returns -EACCES when * trying to execute a directory. */ return status == -EISDIR ? -EACCES : status; - /* user_path is modified only if there's an interpreter - * (ie. for a script or with qemu). */ - if (status == 0 && tracee->qemu == NULL) + bool path_modified = false; + { + int i; + for (i = 0; i < MAXSYMLINKS; i++) { + bool not_modified_here = true; + status = check_binfmt(tracee, host_path, user_path); + if (status < 0) return status == -EISDIR ? -EACCES : status; + if (status > 0) path_modified = true; + if (status > 0) not_modified_here = false; + + status = expand_shebang(tracee, host_path, user_path); + if (status < 0) return status == -EISDIR ? -EACCES : status; + if (status > 0) path_modified = true; + if (status > 0) not_modified_here = false; + + if (not_modified_here) break; + } + if (i == MAXSYMLINKS) { + TALLOC_FREE(raw_path); + return -ELOOP; + } + } + + // user_path is modified only if there's an interpreter + if (!path_modified && tracee->qemu == NULL) TALLOC_FREE(raw_path); /* Remember the new value for "/proc/self/exe". It points to diff --git a/src/execve/shebang.c b/src/execve/shebang.c index 83ceeda1..f6417ca6 100644 --- a/src/execve/shebang.c +++ b/src/execve/shebang.c @@ -27,7 +27,6 @@ #include /* BINPRM_BUF_SIZE, */ #include /* read(2), close(2), */ #include /* -E*, */ -#include /* MAXSYMLINKS, */ #include /* bool, */ #include /* assert(3), */ @@ -208,11 +207,11 @@ static int extract_shebang(const Tracee *tracee UNUSED, const char *host_path, int expand_shebang(Tracee *tracee, char host_path[PATH_MAX], char user_path[PATH_MAX]) { ArrayOfXPointers *argv = NULL; + char *old_user_path = NULL; bool has_shebang = false; char argument[BINPRM_BUF_SIZE]; int status; - size_t i; /* "The interpreter must be a valid pathname for an executable * which is not itself a script [1]. If the filename @@ -228,80 +227,65 @@ int expand_shebang(Tracee *tracee, char host_path[PATH_MAX], char user_path[PATH * ELF interpreter; ie. a script can use a script as * interpreter. */ - for (i = 0; i < MAXSYMLINKS; i++) { - char *old_user_path; - /* Translate this path (user -> host), then check it is executable. */ - status = translate_and_check_exec(tracee, host_path, user_path); - if (status < 0) - return status; + /* Remember the initial user path. */ + old_user_path = talloc_strdup(tracee->ctx, user_path); + if (old_user_path == NULL) + return -ENOMEM; - /* Remember the initial user path. */ - old_user_path = talloc_strdup(tracee->ctx, user_path); - if (old_user_path == NULL) - return -ENOMEM; + /* Extract into user_path and argument the shebang from host_path. */ + status = extract_shebang(tracee, host_path, user_path, argument); + if (status < 0) + return status; - /* Extract into user_path and argument the shebang from host_path. */ - status = extract_shebang(tracee, host_path, user_path, argument); - if (status < 0) - return status; + /* No shebang. */ + if (status == 0) + return 0; + has_shebang = true; - /* No more shebang. */ - if (status == 0) - break; - has_shebang = true; + /* Translate new path (user -> host), then check it is executable. */ + status = translate_and_check_exec(tracee, host_path, user_path); + if (status < 0) + return status; - /* Translate new path (user -> host), then check it is executable. */ - status = translate_and_check_exec(tracee, host_path, user_path); + /* Fetch argv[] only on demand. */ + status = fetch_array_of_xpointers(tracee, &argv, SYSARG_2, 0); + if (status < 0) + return status; + + /* Assuming the shebang of "script" is "#!/bin/sh -x", + * a call to: + * + * execve("./script", { "script.sh", NULL }, ...) + * + * becomes: + * + * execve("/bin/sh", { "/bin/sh", "-x", "./script", NULL }, ...) + * + * See commit 8c8fbe85 about "argv->length == 1". */ + if (argument[0] != '\0') { + status = resize_array_of_xpointers(argv, 0, 2 + (argv->length == 1)); if (status < 0) return status; - /* Fetch argv[] only on demand. */ - if (argv == NULL) { - status = fetch_array_of_xpointers(tracee, &argv, SYSARG_2, 0); - if (status < 0) - return status; - } - - /* Assuming the shebang of "script" is "#!/bin/sh -x", - * a call to: - * - * execve("./script", { "script.sh", NULL }, ...) - * - * becomes: - * - * execve("/bin/sh", { "/bin/sh", "-x", "./script", NULL }, ...) - * - * See commit 8c8fbe85 about "argv->length == 1". */ - if (argument[0] != '\0') { - status = resize_array_of_xpointers(argv, 0, 2 + (argv->length == 1)); - if (status < 0) - return status; - - status = write_xpointees(argv, 0, 3, user_path, argument, old_user_path); - if (status < 0) - return status; - } - else { - status = resize_array_of_xpointers(argv, 0, 1 + (argv->length == 1)); - if (status < 0) - return status; - - status = write_xpointees(argv, 0, 2, user_path, old_user_path); - if (status < 0) - return status; - } + status = write_xpointees(argv, 0, 3, user_path, argument, old_user_path); + if (status < 0) + return status; } + else { + status = resize_array_of_xpointers(argv, 0, 1 + (argv->length == 1)); + if (status < 0) + return status; - if (i == MAXSYMLINKS) - return -ELOOP; - - /* Push argv[] only on demand. */ - if (argv != NULL) { - status = push_array_of_xpointers(argv, SYSARG_2); + status = write_xpointees(argv, 0, 2, user_path, old_user_path); if (status < 0) return status; } + /* Push argv[]. */ + status = push_array_of_xpointers(argv, SYSARG_2); + if (status < 0) + return status; + return (has_shebang ? 1 : 0); }