/* $OpenBSD: filter.c,v 1.33 2006/07/02 12:34:15 sturm Exp $ */ /* * Copyright 2002 Niels Provos * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * This product includes software developed by Niels Provos. * 4. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "intercept.h" #include "systrace.h" #include "filter.h" #include "util.h" extern int allow; extern int noalias; extern int connected; extern int cradle; extern char cwd[]; extern char home[]; extern char username[]; extern char *guipath; int requestor_restart = 0; static void logic_free(struct logic *); static int filter_match(struct intercept_pid *, struct intercept_tlq *, struct logic *); static void filter_review(struct filterq *); static void filter_templates(const char *); static int filter_template(int, struct policy *, int); static int filter_quickpredicate(struct filter *); static void filter_policyrecord(struct policy *, struct filter *, const char *, const char *, char *); static void filter_replace(char *, size_t, char *, char *); static int filter_match(struct intercept_pid *icpid, struct intercept_tlq *tls, struct logic *logic) { struct intercept_translate *tl; int off = 0, res; switch (logic->op) { case LOGIC_NOT: return (!filter_match(icpid, tls, logic->left)); case LOGIC_OR: if (filter_match(icpid, tls, logic->left)) return (1); return (filter_match(icpid, tls, logic->right)); case LOGIC_AND: if (!filter_match(icpid, tls, logic->left)) return (0); return (filter_match(icpid, tls, logic->right)); default: break; } /* Now we just have a logic single */ if (logic->type == NULL) goto match; if (tls == NULL) errx(1, "filter_match has no translators"); TAILQ_FOREACH(tl, tls, next) { if (!tl->trans_valid) continue; if (strcasecmp(tl->name, logic->type)) continue; if (logic->typeoff == -1 || logic->typeoff == off) break; off++; } if (tl == NULL) return (0); match: /* We need to do dynamic expansion on the data */ if (logic->filterdata && (logic->flags & LOGIC_NEEDEXPAND)) { char *old = logic->filterdata; size_t oldlen = logic->filterlen; logic->filterdata = filter_dynamicexpand(icpid, old); logic->filterlen = strlen(logic->filterdata) + 1; res = logic->filter_match(tl, logic); logic->filterdata = old; logic->filterlen = oldlen; } else res = logic->filter_match(tl, logic); return (res); } /* Evaluate filter predicate */ int filter_predicate(struct intercept_pid *icpid, struct predicate *pdc) { id_t pidnr, pdcnr; int res = 0; if (!pdc->p_flags) return (1); if (pdc->p_flags & PREDIC_UID) { pidnr = icpid->uid; pdcnr = pdc->p_uid; } else { pidnr = icpid->gid; pdcnr = pdc->p_gid; } switch (pdc->p_flags & PREDIC_MASK) { case PREDIC_NEGATIVE: res = pidnr != pdcnr; break; case PREDIC_LESSER: res = pidnr < pdcnr; break; case PREDIC_GREATER: res = pidnr > pdcnr; break; default: res = pidnr == pdcnr; break; } return (res); } short filter_evaluate(struct intercept_tlq *tls, struct filterq *fls, struct intercept_pid *icpid) { struct filter *filter, *last = NULL; short action; TAILQ_FOREACH(filter, fls, next) { action = filter->match_action; if (filter_predicate(icpid, &filter->match_predicate) && filter_match(icpid, tls, filter->logicroot)) { /* Profile feedback optimization */ filter->match_count++; if (last != NULL && last->match_action == action && last->match_flags == filter->match_flags && filter->match_count > last->match_count) { TAILQ_REMOVE(fls, last, next); TAILQ_INSERT_AFTER(fls, filter, last, next); } if (action == ICPOLICY_NEVER) action = filter->match_error; icpid->uflags = filter->match_flags; /* Policy requests privilege elevation */ if (filter->elevate.e_flags) icpid->elevate = &filter->elevate; return (action); } /* Keep track of last processed filtered in a group */ last = filter; } return (ICPOLICY_ASK); } static void logic_free(struct logic *logic) { if (logic->left) logic_free(logic->left); if (logic->right) logic_free(logic->right); if (logic->type) free(logic->type); if (logic->filterdata) free(logic->filterdata); free(logic); } void filter_free(struct filter *filter) { if (filter->logicroot) logic_free(filter->logicroot); if (filter->rule) free(filter->rule); free(filter); } static void filter_review(struct filterq *fls) { struct filter *filter; int i = 0; printf("Filter review:\n"); TAILQ_FOREACH(filter, fls, next) { i++; printf("%d. %s\n", i, filter->rule); } } static void filter_templates(const char *emulation) { extern struct tmplqueue templates; struct template *template; int i = 0; printf("Available Templates:\n"); TAILQ_FOREACH(template, &templates, next) { if (strcmp(template->emulation, emulation)) continue; i++; printf("%d. %s - %s\n", i, template->name, template->description); } } /* Inserts a policy from a template */ static int filter_template(int fd, struct policy *policy, int count) { extern struct tmplqueue templates; struct template *template; int i = 0; TAILQ_FOREACH(template, &templates, next) { if (strcmp(template->emulation, policy->emulation)) continue; i++; if (i == count) break; } if (i != count) return (-1); template = systrace_readtemplate(template->filename, policy, template); if (template == NULL) return (-1); if (filter_prepolicy(fd, policy) == -1) return (-1); /* We inserted new statements into the policy */ policy->flags |= POLICY_CHANGED; return (0); } static void filter_policyrecord(struct policy *policy, struct filter *filter, const char *emulation, const char *name, char *rule) { /* Record the filter in the policy */ filter = calloc(1, sizeof(struct filter)); if (filter == NULL) err(1, "%s:%d: calloc", __func__, __LINE__); if ((filter->rule = strdup(rule)) == NULL) err(1, "%s:%d: strdup", __func__, __LINE__); strlcpy(filter->name, name, sizeof(filter->name)); strlcpy(filter->emulation, emulation, sizeof(filter->emulation)); TAILQ_INSERT_TAIL(&policy->filters, filter, policy_next); policy->nfilters++; policy->flags |= POLICY_CHANGED; } int filter_parse(char *line, struct filter **pfilter) { char *rule; if (parse_filter(line, pfilter) == -1) return (-1); if ((rule = strdup(line)) == NULL) err(1, "%s:%d: strdup", __func__, __LINE__); (*pfilter)->rule = rule; return (0); } /* Translate a simple action like "permit" or "deny[einval]" to numbers */ int filter_parse_simple(char *rule, short *paction, short *pfuture) { char buf[_POSIX2_LINE_MAX]; int isfuture = 1; char *line, *p; if (strlcpy(buf, rule, sizeof(buf)) >= sizeof(buf)) return (-1); line = buf; if (!strcmp("permit", line)) { *paction = *pfuture = ICPOLICY_PERMIT; return (0); } else if (!strcmp("permit-now", line)) { *paction = ICPOLICY_PERMIT; return (0); } else if (strncmp("deny", line, 4)) return (-1); line +=4 ; if (!strncmp("-now", line, 4)) { line += 4; isfuture = 0; } *paction = ICPOLICY_NEVER; switch (line[0]) { case '\0': break; case '[': line++; p = strsep(&line, "]"); if (line == NULL || *line != '\0') return (-1); *paction = systrace_error_translate(p); if (*paction == -1) return (-1); break; default: return (-1); } if (isfuture) *pfuture = *paction; return (0); } void filter_modifypolicy(int fd, int policynr, const char *emulation, const char *name, short future) { struct systrace_revalias *reverse = NULL; /* * Check if we are dealing with a system call that really * is an alias for something else. */ if (!noalias) reverse = systrace_find_reverse(emulation, name); if (reverse == NULL) { if (systrace_modifypolicy(fd, policynr, name, future) == -1) errx(1, "%s:%d: modify policy for %s-%s", __func__, __LINE__, emulation, name); } else { struct systrace_alias *alias; /* For every system call associated with this alias * set the permanent in-kernel policy. */ TAILQ_FOREACH(alias, &reverse->revl, next) { if(systrace_modifypolicy(fd, policynr, alias->name, future) == -1) errx(1, "%s:%d: modify policy for %s-%s", __func__, __LINE__, emulation, alias->name); } } } /* In non-root case, evaluate predicates early */ static int filter_quickpredicate(struct filter *filter) { struct predicate *pdc; struct intercept_pid icpid; pdc = &filter->match_predicate; if (!pdc->p_flags) return (1); intercept_setpid(&icpid, getuid(), getgid()); if (!filter_predicate(&icpid, pdc)) return (0); memset(pdc, 0, sizeof(filter->match_predicate)); return (1); } /* * Processes the filters for a policy that have not been applied yet. * Pre-filters get installed when reading a policy. This function * installs a fast-path in the kernel. */ int filter_prepolicy(int fd, struct policy *policy) { int res; struct filter *filter, *parsed; struct filterq *fls; short action, future; extern int iamroot; /* Commit all matching pre-filters */ for (filter = TAILQ_FIRST(&policy->prefilters); filter; filter = TAILQ_FIRST(&policy->prefilters)) { future = ICPOLICY_ASK; TAILQ_REMOVE(&policy->prefilters, filter, policy_next); res = 0; parsed = NULL; /* Special rules that are not real filters */ if (filter_parse_simple(filter->rule, &action, &future) == -1) res = filter_parse(filter->rule, &parsed); if (res == -1) errx(1, "%s:%d: can not parse \"%s\"", __func__, __LINE__, filter->rule); if (future == ICPOLICY_ASK) { if (iamroot || filter_quickpredicate(parsed)) { fls = systrace_policyflq(policy, policy->emulation, filter->name); TAILQ_INSERT_TAIL(fls, parsed, next); } } else { filter_modifypolicy(fd, policy->policynr, policy->emulation, filter->name, future); } filter_policyrecord(policy, parsed, policy->emulation, filter->name, filter->rule); filter_free(filter); } /* Existing policy applied undo changed flag */ policy->flags &= ~POLICY_CHANGED; return (0); } short filter_ask(int fd, struct intercept_tlq *tls, struct filterq *fls, int policynr, const char *emulation, const char *name, char *output, short *pfuture, struct intercept_pid *icpid) { char line[2*MAXPATHLEN], *p; char compose[2*MAXPATHLEN]; struct filter *filter; struct policy *policy; short action; int first = 1, isalias, isprompt = 0; *pfuture = ICPOLICY_ASK; isalias = systrace_find_reverse(emulation, name) != NULL; if ((policy = systrace_findpolnr(policynr)) == NULL) errx(1, "%s:%d: no policy %d", __func__, __LINE__, policynr); if (!allow) printf("%s\n", output); else { /* Automatically allow */ if (tls != NULL) { struct intercept_translate *tl; char *l, *lst = NULL; int set = 0; /* Explicitly match every component */ line[0] = '\0'; TAILQ_FOREACH(tl, tls, next) { if (!tl->trans_valid) continue; l = intercept_translate_print(tl); if (l == NULL) continue; snprintf(compose, sizeof(compose), "%s%s eq \"%s\"", tl->name, lst && !strcmp(tl->name, lst) ? "[1]" : "", strescape(l)); lst = tl->name; if (set) strlcat(line, " and ", sizeof(line)); else set = 1; strlcat(line, compose, sizeof(line)); } if (!set) strlcpy(line, "true", sizeof(line)); strlcat(line, " then permit", sizeof(line)); } else strlcpy(line, "permit", sizeof(line)); } while (1) { /* Special policy active that allows only yes or no */ if (icpid->uflags & PROCESS_PROMPT) isprompt = 1; filter = NULL; if (!allow) { /* Ask for a policy */ if (!connected) printf("Answer: "); else { /* Do not prompt the first time */ if (!first) { printf("WRONG\n"); } } if (fgets(line, sizeof(line), stdin) == NULL) { if (connected && !cradle && errno == EPIPE && !requestor_restart) { requestor_start(guipath, 0); clearerr(stdin); clearerr(stdout); requestor_restart = 1; printf("%s\n", output); continue; } err(1, "EOF on policy input request"); } p = line; strsep(&p, "\n"); } else if (!first) { /* Error with filter */ errx(1, "Filter generation error: %s", line); } first = 0; requestor_restart = 0; /* Simple keywords */ if (!strcasecmp(line, "detach")) { if (policy->nfilters) { policy->flags |= POLICY_UNSUPERVISED; action = ICPOLICY_NEVER; } else { policy->flags |= POLICY_DETACHED; policy->flags |= POLICY_CHANGED; action = ICPOLICY_PERMIT; } goto out; } else if (!strcasecmp(line, "kill")) { action = ICPOLICY_KILL; goto out; } else if (!strcasecmp(line, "review") && fls != NULL) { filter_review(fls); continue; } else if (!strcasecmp(line, "templates")) { filter_templates(emulation); continue; } else if (!strncasecmp(line, "template ", 9)) { int count = atoi(line + 9); if (count == 0 || filter_template(fd, policy, count) == -1) { printf("Syntax error.\n"); continue; } if (fls != NULL) action = filter_evaluate(tls, fls, icpid); else action = ICPOLICY_PERMIT; if (action == ICPOLICY_ASK) { printf("Filter unmatched.\n"); continue; } goto out; } if (filter_parse_simple(line, &action, pfuture) != -1) { /* Yes or no, no in-kernel policy allowed */ if (isprompt) *pfuture = ICPOLICY_ASK; if (*pfuture == ICPOLICY_ASK) goto out; /* We have a policy decision */ if (!isalias) break; /* No in-kernel policy for aliases */ strlcpy(compose, line, sizeof(compose)); /* Change into userland rule */ snprintf(line, sizeof(line), "true then %s", compose); } if (isprompt) { printf("Answer only \"permit\" or \"deny\". " "This is a prompt.\n"); continue; } if (fls == NULL) { printf("Syntax error.\n"); continue; } if (filter_parse(line, &filter) == -1) { printf("Parse error.\n"); continue; } TAILQ_INSERT_TAIL(fls, filter, next); action = filter_evaluate(tls, fls, icpid); /* If we get a prompt flag here, we ask again */ if (icpid->uflags & PROCESS_PROMPT) { filter_policyrecord(policy, filter, emulation, name, line); printf("Answer only \"permit\" or \"deny\". " "This is a prompt.\n"); continue; } if (action == ICPOLICY_ASK) { TAILQ_REMOVE(fls, filter, next); printf("Filter unmatched. Freeing it\n"); filter_free(filter); continue; } break; } filter_policyrecord(policy, filter, emulation, name, line); out: if (connected) printf("OKAY\n"); return (action); } static void filter_replace(char *buf, size_t buflen, char *match, char *repl) { while (strrpl(buf, buflen, match, repl) != NULL) ; } char * filter_expand(char *data) { static char expand[2*MAXPATHLEN]; strlcpy(expand, data, sizeof(expand)); filter_replace(expand, sizeof(expand), "$HOME", home); filter_replace(expand, sizeof(expand), "$USER", username); filter_replace(expand, sizeof(expand), "$CWD", cwd); return (expand); } char * filter_dynamicexpand(struct intercept_pid *icpid, char *data) { extern char cwd[]; static char expand[2*MAXPATHLEN]; strlcpy(expand, data, sizeof(expand)); filter_replace(expand, sizeof(expand), "$HOME", icpid->home); filter_replace(expand, sizeof(expand), "$USER", icpid->username); filter_replace(expand, sizeof(expand), "$CWD", cwd); return (expand); } /* Checks if the string needs expansion */ int filter_needexpand(char *data) { if (strstr(data, "$HOME") != NULL) return (1); if (strstr(data, "$USER") != NULL) return (1); if (strstr(data, "$CWD") != NULL) return (1); return (0); } int filter_fnmatch(struct intercept_translate *tl, struct logic *logic) { int res; char *line; if ((line = intercept_translate_print(tl)) == NULL) return (0); res = fnmatch(logic->filterdata, line, FNM_PATHNAME | FNM_LEADING_DIR); return (res == 0); } int filter_substrmatch(struct intercept_translate *tl, struct logic *logic) { char *line; if ((line = intercept_translate_print(tl)) == NULL) return (0); return (strstr(line, logic->filterdata) != NULL); } int filter_negsubstrmatch(struct intercept_translate *tl, struct logic *logic) { char *line; if ((line = intercept_translate_print(tl)) == NULL) return (0); return (strstr(line, logic->filterdata) == NULL); } int filter_stringmatch(struct intercept_translate *tl, struct logic *logic) { char *line; if ((line = intercept_translate_print(tl)) == NULL) return (0); return (!strcasecmp(line, logic->filterdata)); } int filter_negstringmatch(struct intercept_translate *tl, struct logic *logic) { char *line; if ((line = intercept_translate_print(tl)) == NULL) return (1); return (strcasecmp(line, logic->filterdata) != 0); } int filter_inpath(struct intercept_translate *tl, struct logic *logic) { char *line, c; int len; if ((line = intercept_translate_print(tl)) == NULL) return (0); len = strlen(line); if (len == 0 || len > strlen(logic->filterdata)) return (0); /* Root is always in path */ if (len == 1) return (line[0] == '/'); /* Complete filename needs to fit */ if (strncmp(line, logic->filterdata, len)) return (0); /* Termination has to be \0 or / */ c = ((char *)logic->filterdata)[len]; if (c != '/' && c != '\0') return (0); return (1); } int filter_regex(struct intercept_translate *tl, struct logic *logic) { regex_t tmpre, *re; char *line; int res; if ((line = intercept_translate_print(tl)) == NULL) return (0); re = logic->filterarg; if (re == NULL) { /* If regex does not compute, we just do not match */ if (regcomp(&tmpre, logic->filterdata, REG_EXTENDED | REG_NOSUB) != 0) return (0); re = &tmpre; } res = regexec(re, line, 0, NULL, 0); /* Clean up temporary memory associated with regex */ if (re == &tmpre) regfree(re); return (res == 0); } /* ARGSUSED */ int filter_true(struct intercept_translate *tl, struct logic *logic) { return (1); }