Changeset View
Changeset View
Standalone View
Standalone View
head/sys/security/mac_veriexec_parser/mac_veriexec_parser.c
Property | Old Value | New Value |
---|---|---|
svn:keywords | null | FreeBSD=%H \ No newline at end of property |
/*- | |||||
* Copyright (c) 2019 Stormshield. | |||||
* Copyright (c) 2019 Semihalf. | |||||
* | |||||
* 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. | |||||
* | |||||
* 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 <sys/cdefs.h> | |||||
__FBSDID("$FreeBSD$"); | |||||
#include <sys/param.h> | |||||
#include <sys/ctype.h> | |||||
#include <sys/eventhandler.h> | |||||
#include <sys/fcntl.h> | |||||
#include <sys/lock.h> | |||||
#include <sys/module.h> | |||||
#include <sys/mutex.h> | |||||
#include <sys/namei.h> | |||||
#include <sys/proc.h> | |||||
#include <sys/systm.h> | |||||
#include <sys/vnode.h> | |||||
#include <crypto/sha2/sha256.h> | |||||
#include <crypto/sha2/sha384.h> | |||||
#include <crypto/sha2/sha512.h> | |||||
#include <security/mac_veriexec/mac_veriexec.h> | |||||
#include <security/mac_veriexec/mac_veriexec_internal.h> | |||||
/* The following are based on sbin/veriexec */ | |||||
struct fingerprint_type { | |||||
const char *fp_type; | |||||
int fp_size; | |||||
}; | |||||
struct fp_flag { | |||||
const char *flag_name; | |||||
int flag; | |||||
}; | |||||
static const struct fingerprint_type fp_table[] = { | |||||
{"sha256=", SHA256_DIGEST_LENGTH}, | |||||
#if MAXFINGERPRINTLEN >= SHA384_DIGEST_LENGTH | |||||
{"sha384=", SHA384_DIGEST_LENGTH}, | |||||
#endif | |||||
#if MAXFINGERPRINTLEN >= SHA512_DIGEST_LENGTH | |||||
{"sha512=", SHA512_DIGEST_LENGTH}, | |||||
#endif | |||||
{NULL, 0} | |||||
}; | |||||
static const struct fp_flag flags_table[] = { | |||||
{"indirect", VERIEXEC_INDIRECT}, | |||||
{"no_ptrace", VERIEXEC_NOTRACE}, | |||||
{"trusted", VERIEXEC_TRUSTED}, | |||||
{"no_fips", VERIEXEC_NOFIPS}, | |||||
{NULL, 0} | |||||
}; | |||||
extern struct mtx ve_mutex; | |||||
static unsigned char hexchar_to_byte(unsigned char c); | |||||
static int hexstring_to_bin(unsigned char *buf); | |||||
static int get_flags(const char *entry); | |||||
static int get_fp(const char *entry, char **type, | |||||
unsigned char **digest, int *flags); | |||||
static int verify_digest(const char *data, size_t len, | |||||
const unsigned char *expected_hash); | |||||
static int open_file(const char *path, struct nameidata *nid); | |||||
static char *read_manifest(char *path, unsigned char *digest); | |||||
static int parse_entry(char *entry, char *prefix); | |||||
static int parse_manifest(char *path, unsigned char *hash, char *prefix); | |||||
static unsigned char | |||||
hexchar_to_byte(unsigned char c) | |||||
{ | |||||
if (isdigit(c)) | |||||
return (c - '0'); | |||||
return (isupper(c) ? c - 'A' + 10 : c - 'a' + 10); | |||||
} | |||||
static int | |||||
hexstring_to_bin(unsigned char *buf) | |||||
{ | |||||
size_t i, len; | |||||
unsigned char byte; | |||||
len = strlen(buf); | |||||
for (i = 0; i < len / 2; i++) { | |||||
if (!isxdigit(buf[2 * i]) || !isxdigit(buf[2 * i + 1])) | |||||
return (EINVAL); | |||||
byte = hexchar_to_byte(buf[2 * i]) << 4; | |||||
byte += hexchar_to_byte(buf[2 * i + 1]); | |||||
buf[i] = byte; | |||||
} | |||||
return (0); | |||||
} | |||||
static int | |||||
get_flags(const char *entry) | |||||
{ | |||||
int i; | |||||
int result = 0; | |||||
for (i = 0; flags_table[i].flag_name != NULL; i++) | |||||
if (strstr(entry, flags_table[i].flag_name) != NULL) | |||||
result |= flags_table[i].flag; | |||||
return (result); | |||||
} | |||||
/* | |||||
* Parse a single line of manifest looking for a digest and its type. | |||||
* We expect it to be in form of "path shaX=hash". | |||||
* The line will be split into path, hash type and hash value. | |||||
*/ | |||||
static int | |||||
get_fp(const char *entry, char **type, unsigned char **digest, int *flags) | |||||
{ | |||||
char *delimiter; | |||||
char *local_digest; | |||||
char *fp_type; | |||||
char *prev_fp_type; | |||||
size_t min_len; | |||||
int i; | |||||
delimiter = NULL; | |||||
fp_type = NULL; | |||||
prev_fp_type = NULL; | |||||
for (i = 0; fp_table[i].fp_type != NULL; i++) { | |||||
fp_type = strstr(entry, fp_table[i].fp_type); | |||||
/* Look for the last "shaX=hash" in line */ | |||||
while (fp_type != NULL) { | |||||
prev_fp_type = fp_type; | |||||
fp_type++; | |||||
fp_type = strstr(fp_type, fp_table[i].fp_type); | |||||
} | |||||
fp_type = prev_fp_type; | |||||
if (fp_type != NULL) { | |||||
if (fp_type == entry || fp_type[-1] != ' ') | |||||
return (EINVAL); | |||||
/* | |||||
* The entry should contain at least | |||||
* fp_type and digest in hexadecimal form. | |||||
*/ | |||||
min_len = strlen(fp_table[i].fp_type) + | |||||
2 * fp_table[i].fp_size; | |||||
if (strnlen(fp_type, min_len) < min_len) | |||||
return (EINVAL); | |||||
local_digest = &fp_type[strlen(fp_table[i].fp_type)]; | |||||
delimiter = &local_digest[2 * fp_table[i].fp_size]; | |||||
/* | |||||
* Make sure that digest is followed by | |||||
* some kind of delimiter. | |||||
*/ | |||||
if (*delimiter != '\n' && | |||||
*delimiter != '\0' && | |||||
*delimiter != ' ') | |||||
return (EINVAL); | |||||
/* | |||||
* Does the entry contain flags we need to parse? | |||||
*/ | |||||
if (*delimiter == ' ' && flags != NULL) | |||||
*flags = get_flags(delimiter); | |||||
/* | |||||
* Split entry into three parts: | |||||
* path, fp_type and digest. | |||||
*/ | |||||
local_digest[-1] = '\0'; | |||||
*delimiter = '\0'; | |||||
fp_type[-1] = '\0'; | |||||
break; | |||||
} | |||||
} | |||||
if (fp_type == NULL) | |||||
return (EINVAL); | |||||
if (type != NULL) | |||||
*type = fp_type; | |||||
if (digest != NULL) | |||||
*digest = local_digest; | |||||
return (0); | |||||
} | |||||
/* | |||||
* Currently we verify manifest using sha256. | |||||
* In future another env with hash type could be introduced. | |||||
*/ | |||||
static int | |||||
verify_digest(const char *data, size_t len, const unsigned char *expected_hash) | |||||
{ | |||||
SHA256_CTX ctx; | |||||
unsigned char hash[SHA256_DIGEST_LENGTH]; | |||||
SHA256_Init(&ctx); | |||||
SHA256_Update(&ctx, data, len); | |||||
SHA256_Final(hash, &ctx); | |||||
return (memcmp(expected_hash, hash, SHA256_DIGEST_LENGTH)); | |||||
} | |||||
static int | |||||
open_file(const char *path, struct nameidata *nid) | |||||
{ | |||||
int flags, rc; | |||||
flags = FREAD; | |||||
pwd_ensure_dirs(); | |||||
NDINIT(nid, LOOKUP, 0, UIO_SYSSPACE, path, curthread); | |||||
rc = vn_open(nid, &flags, 0, NULL); | |||||
NDFREE(nid, NDF_ONLY_PNBUF); | |||||
if (rc != 0) | |||||
return (rc); | |||||
return (0); | |||||
} | |||||
/* | |||||
* Read the manifest from location specified in path and verify its digest. | |||||
*/ | |||||
static char* | |||||
read_manifest(char *path, unsigned char *digest) | |||||
{ | |||||
struct nameidata nid; | |||||
struct vattr va; | |||||
char *data; | |||||
ssize_t bytes_read, resid; | |||||
int rc; | |||||
data = NULL; | |||||
bytes_read = 0; | |||||
rc = open_file(path, &nid); | |||||
if (rc != 0) | |||||
goto fail; | |||||
rc = VOP_GETATTR(nid.ni_vp, &va, curthread->td_ucred); | |||||
if (rc != 0) | |||||
goto fail; | |||||
data = (char *)malloc(va.va_size + 1, M_VERIEXEC, M_WAITOK); | |||||
while (bytes_read < va.va_size) { | |||||
rc = vn_rdwr( | |||||
UIO_READ, nid.ni_vp, data, | |||||
va.va_size - bytes_read, bytes_read, | |||||
UIO_SYSSPACE, IO_NODELOCKED, | |||||
curthread->td_ucred, NOCRED, &resid, curthread); | |||||
if (rc != 0) | |||||
goto fail; | |||||
bytes_read = va.va_size - resid; | |||||
} | |||||
data[bytes_read] = '\0'; | |||||
VOP_UNLOCK(nid.ni_vp, 0); | |||||
(void)vn_close(nid.ni_vp, FREAD, curthread->td_ucred, curthread); | |||||
/* | |||||
* If digest is wrong someone might be trying to fool us. | |||||
*/ | |||||
if (verify_digest(data, va.va_size, digest)) | |||||
panic("Manifest hash doesn't match expected value!"); | |||||
return (data); | |||||
fail: | |||||
if (data != NULL) | |||||
free(data, M_VERIEXEC); | |||||
return (NULL); | |||||
} | |||||
/* | |||||
* Process single line. | |||||
* First split it into path, digest_type and digest. | |||||
* Then try to open the file and insert its fingerprint into metadata store. | |||||
*/ | |||||
static int | |||||
parse_entry(char *entry, char *prefix) | |||||
{ | |||||
struct nameidata nid; | |||||
struct vattr va; | |||||
char path[MAXPATHLEN]; | |||||
char *fp_type; | |||||
unsigned char *digest; | |||||
int rc, is_exec, flags; | |||||
fp_type = NULL; | |||||
digest = NULL; | |||||
flags = 0; | |||||
rc = get_fp(entry, &fp_type, &digest, &flags); | |||||
if (rc != 0) | |||||
return (rc); | |||||
rc = hexstring_to_bin(digest); | |||||
if (rc != 0) | |||||
return (rc); | |||||
if (strnlen(entry, MAXPATHLEN) == MAXPATHLEN) | |||||
return (EINVAL); | |||||
/* If the path is not absolute prepend it with a prefix */ | |||||
if (prefix != NULL && entry[0] != '/') { | |||||
rc = snprintf(path, MAXPATHLEN, "%s/%s", | |||||
prefix, entry); | |||||
if (rc < 0) | |||||
return (-rc); | |||||
} else { | |||||
strcpy(path, entry); | |||||
} | |||||
rc = open_file(path, &nid); | |||||
NDFREE(&nid, NDF_ONLY_PNBUF); | |||||
if (rc != 0) | |||||
return (rc); | |||||
rc = VOP_GETATTR(nid.ni_vp, &va, curthread->td_ucred); | |||||
if (rc != 0) | |||||
goto out; | |||||
is_exec = (va.va_mode & VEXEC); | |||||
mtx_lock(&ve_mutex); | |||||
rc = mac_veriexec_metadata_add_file( | |||||
is_exec == 0, | |||||
va.va_fsid, va.va_fileid, va.va_gen, | |||||
digest, flags, fp_type, 1); | |||||
mtx_unlock(&ve_mutex); | |||||
out: | |||||
VOP_UNLOCK(nid.ni_vp, 0); | |||||
vn_close(nid.ni_vp, FREAD, curthread->td_ucred, curthread); | |||||
return (rc); | |||||
} | |||||
/* | |||||
* Look for manifest in env that have beed passed by loader. | |||||
* This routine should be called right after the rootfs is mounted. | |||||
*/ | |||||
static int | |||||
parse_manifest(char *path, unsigned char *hash, char *prefix) | |||||
{ | |||||
char *data; | |||||
char *entry; | |||||
char *next_entry; | |||||
int rc, success_count; | |||||
data = NULL; | |||||
success_count = 0; | |||||
rc = 0; | |||||
data = read_manifest(path, hash); | |||||
if (data == NULL) { | |||||
rc = EIO; | |||||
goto out; | |||||
} | |||||
entry = data; | |||||
while (entry != NULL) { | |||||
next_entry = strchr(entry, '\n'); | |||||
if (next_entry != NULL) { | |||||
*next_entry = '\0'; | |||||
next_entry++; | |||||
} | |||||
if (entry[0] == '\n' || entry[0] == '\0') { | |||||
entry = next_entry; | |||||
continue; | |||||
} | |||||
if ((rc = parse_entry(entry, prefix))) | |||||
printf("mac_veriexec_parser: Warning: Failed to parse" | |||||
" entry with rc:%d, entry:\"%s\"\n", rc, entry); | |||||
else | |||||
success_count++; | |||||
entry = next_entry; | |||||
} | |||||
rc = 0; | |||||
out: | |||||
if (data != NULL) | |||||
free(data, M_VERIEXEC); | |||||
if (success_count == 0) | |||||
rc = EINVAL; | |||||
return (rc); | |||||
} | |||||
static void | |||||
parse_manifest_event(void *dummy) | |||||
{ | |||||
char *manifest_path; | |||||
char *manifest_prefix; | |||||
unsigned char *manifest_hash; | |||||
int rc; | |||||
/* If the envs are not set fail silently */ | |||||
manifest_path = kern_getenv("veriexec.manifest_path"); | |||||
if (manifest_path == NULL) | |||||
return; | |||||
manifest_hash = kern_getenv("veriexec.manifest_hash"); | |||||
if (manifest_hash == NULL) { | |||||
freeenv(manifest_path); | |||||
return; | |||||
} | |||||
manifest_prefix = kern_getenv("veriexec.manifest_prefix"); | |||||
if (strlen(manifest_hash) != 2 * SHA256_DIGEST_LENGTH) | |||||
panic("veriexec.manifest_hash has incorrect size"); | |||||
rc = hexstring_to_bin(manifest_hash); | |||||
if (rc != 0) | |||||
panic("mac_veriexec: veriexec.loader.manifest_hash" | |||||
" doesn't contain a hash in hexadecimal form"); | |||||
rc = parse_manifest(manifest_path, manifest_hash, manifest_prefix); | |||||
if (rc != 0) | |||||
panic("mac_veriexec: Failed to parse manifest err=%d", rc); | |||||
mtx_lock(&ve_mutex); | |||||
mac_veriexec_set_state( | |||||
VERIEXEC_STATE_LOADED | VERIEXEC_STATE_ACTIVE | | |||||
VERIEXEC_STATE_LOCKED | VERIEXEC_STATE_ENFORCE); | |||||
mtx_unlock(&ve_mutex); | |||||
freeenv(manifest_path); | |||||
freeenv(manifest_hash); | |||||
if (manifest_prefix != NULL) | |||||
freeenv(manifest_prefix); | |||||
} | |||||
EVENTHANDLER_DEFINE(mountroot, parse_manifest_event, NULL, 0); |