X-Git-Url: https://git.saurik.com/apple/xnu.git/blobdiff_plain/316670eb35587141e969394ae8537d66b9211e80..4d15aeb193b2c68f1d38666c317f8d3734f5f083:/bsd/kern/imageboot.c diff --git a/bsd/kern/imageboot.c b/bsd/kern/imageboot.c index 9fa89e443..6ba54a69f 100644 --- a/bsd/kern/imageboot.c +++ b/bsd/kern/imageboot.c @@ -37,7 +37,19 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + #include +#include extern struct filedesc filedesc0; @@ -85,12 +97,13 @@ imageboot_needed(void) panic("%s: M_NAMEI zone exhausted", __FUNCTION__); /* Check for first layer */ - if (!(PE_parse_boot_argn("rp0", root_path, MAXPATHLEN) || + if (!(PE_parse_boot_argn("rp0", root_path, MAXPATHLEN) || PE_parse_boot_argn("rp", root_path, MAXPATHLEN) || - PE_parse_boot_argn(IMAGEBOOT_ROOT_ARG, root_path, MAXPATHLEN))) { + PE_parse_boot_argn(IMAGEBOOT_ROOT_ARG, root_path, MAXPATHLEN) || + PE_parse_boot_argn(IMAGEBOOT_AUTHROOT_ARG, root_path, MAXPATHLEN))) { goto out; } - + /* Sanity-check first layer */ if (imageboot_format_is_valid(root_path)) { DBG_TRACE("%s: Found %s\n", __FUNCTION__, root_path); @@ -202,18 +215,635 @@ imageboot_mount_image(const char *root_path, int height) return 0; } -static boolean_t + +/* + * Authenticated root-dmg support + */ + +#define AUTHDBG(fmt, args...) do { printf("%s: " fmt "\n", __func__, ##args); } while (0) +#define AUTHPRNT(fmt, args...) do { printf("%s: " fmt "\n", __func__, ##args); } while (0) + +#define kfree_safe(x) do { if ((x)) { kfree_addr((x)); (x) = NULL; } } while (0) + +enum { + MISSING_SIG = -1, + INVALID_SIG = -2 +}; + +static void +key_byteswap(void *_dst, const void *_src, size_t len) +{ + uint32_t *dst __attribute__((align_value(1))) = _dst; + const uint32_t *src __attribute__((align_value(1))) = _src; + + assert(len % sizeof(uint32_t) == 0); + + len = len / sizeof(uint32_t); + for (size_t i = 0; i < len; i++) { + dst[len-i-1] = OSSwapInt32(src[i]); + } +} + +static int +read_file(const char *path, void **bufp, size_t *bufszp) +{ + int err = 0; + struct nameidata ndp = {}; + struct vnode *vp = NULL; + off_t fsize = 0; + int resid = 0; + char *buf = NULL; + bool doclose = false; + + vfs_context_t ctx = vfs_context_kernel(); + proc_t p = vfs_context_proc(ctx); + kauth_cred_t kerncred = vfs_context_ucred(ctx); + + NDINIT(&ndp, LOOKUP, OP_OPEN, LOCKLEAF, UIO_SYSSPACE, path, ctx); + if ((err = namei(&ndp)) != 0) { + AUTHPRNT("namei failed (%s)", path); + goto out; + } + nameidone(&ndp); + vp = ndp.ni_vp; + + if ((err = vnode_size(vp, &fsize, ctx)) != 0) { + AUTHPRNT("failed to get vnode size"); + goto out; + } + if (fsize < 0) { + panic("negative file size"); + } + + if ((err = VNOP_OPEN(vp, FREAD, ctx)) != 0) { + AUTHPRNT("failed to open vnode"); + goto out; + } + doclose = true; + + /* if bufsz is non-zero, cap the read at bufsz bytes */ + if (*bufszp && *bufszp < (size_t)fsize) { + fsize = *bufszp; + } + + buf = kalloc(fsize); + if (buf == NULL) { + err = ENOMEM; + goto out; + } + + if ((err = vn_rdwr(UIO_READ, vp, (caddr_t)buf, fsize, 0, UIO_SYSSPACE, IO_NODELOCKED, kerncred, &resid, p)) != 0) { + AUTHPRNT("vn_rdwr() failed"); + goto out; + } + + if (resid) { + /* didnt get everything we wanted */ + AUTHPRNT("vn_rdwr resid = %d", resid); + err = EINVAL; + goto out; + } + +out: + if (doclose) { + VNOP_CLOSE(vp, FREAD, ctx); + } + if (vp) { + vnode_put(vp); + vp = NULL; + } + + if (err) { + kfree_safe(buf); + } else { + *bufp = buf; + *bufszp = fsize; + } + + return err; +} + +static int +validate_signature(const uint8_t *key_msb, size_t keylen, uint8_t *sig_msb, size_t siglen, uint8_t *digest) +{ + int err = 0; + bool sig_valid = false; + uint8_t *sig = NULL; + + const uint8_t exponent[] = { 0x01, 0x00, 0x01 }; + uint8_t *modulus = kalloc(keylen); + rsa_pub_ctx *rsa_ctx = kalloc(sizeof(rsa_pub_ctx)); + sig = kalloc(siglen); + + if (modulus == NULL || rsa_ctx == NULL || sig == NULL) { + err = ENOMEM; + goto out; + } + + bzero(rsa_ctx, sizeof(rsa_pub_ctx)); + key_byteswap(modulus, key_msb, keylen); + key_byteswap(sig, sig_msb, siglen); + + err = rsa_make_pub(rsa_ctx, + sizeof(exponent), exponent, + CHUNKLIST_PUBKEY_LEN, modulus); + if (err) { + AUTHPRNT("rsa_make_pub() failed"); + goto out; + } + + err = rsa_verify_pkcs1v15(rsa_ctx, CC_DIGEST_OID_SHA256, + SHA256_DIGEST_LENGTH, digest, + siglen, sig, + &sig_valid); + if (err) { + sig_valid = false; + AUTHPRNT("rsa_verify() failed"); + err = EINVAL; + goto out; + } + +out: + kfree_safe(sig); + kfree_safe(rsa_ctx); + kfree_safe(modulus); + + if (err) { + return err; + } else if (sig_valid == true) { + return 0; /* success */ + } else { + return INVALID_SIG; + } +} + +static int +validate_chunklist(void *buf, size_t len) +{ + int err = 0; + size_t sigsz = 0; + size_t sig_end = 0; + size_t chunks_end = 0; + bool valid_sig = false; + struct chunklist_hdr *hdr = buf; + + if (len < sizeof(struct chunklist_hdr)) { + AUTHPRNT("no space for header"); + return EINVAL; + } + + /* recognized file format? */ + if (hdr->cl_magic != CHUNKLIST_MAGIC || + hdr->cl_file_ver != CHUNKLIST_FILE_VERSION_10 || + hdr->cl_chunk_method != CHUNKLIST_SIGNATURE_METHOD_10 || + hdr->cl_sig_method != CHUNKLIST_SIGNATURE_METHOD_10) { + AUTHPRNT("unrecognized chunklist format"); + return EINVAL; + } + + /* does the chunk list fall within the bounds of the buffer? */ + if (os_mul_and_add_overflow(hdr->cl_chunk_count, sizeof(struct chunklist_chunk), hdr->cl_chunk_offset, &chunks_end) || + hdr->cl_chunk_offset < sizeof(struct chunklist_hdr) || chunks_end > len) { + AUTHPRNT("invalid chunk_count (%llu) or chunk_offset (%llu)", + hdr->cl_chunk_count, hdr->cl_chunk_offset); + return EINVAL; + } + + /* does the signature fall within the bounds of the buffer? */ + if (os_add_overflow(hdr->cl_sig_offset, sizeof(struct chunklist_sig), &sig_end) || + hdr->cl_sig_offset < sizeof(struct chunklist_hdr) || + hdr->cl_sig_offset < chunks_end || + hdr->cl_sig_offset > len) { + AUTHPRNT("invalid signature offset (%llu)", hdr->cl_sig_offset); + return EINVAL; + } + + if (sig_end > len || os_sub_overflow(len, hdr->cl_sig_offset, &sigsz) || sigsz != CHUNKLIST_SIG_LEN) { + /* missing or incorrect signature size */ + return MISSING_SIG; + } + + AUTHDBG("hashing chunklist"); + + /* hash the chunklist (excluding the signature) */ + uint8_t sha_digest[SHA256_DIGEST_LENGTH]; + SHA256_CTX sha_ctx; + SHA256_Init(&sha_ctx); + SHA256_Update(&sha_ctx, buf, hdr->cl_sig_offset); + SHA256_Final(sha_digest, &sha_ctx); + + AUTHDBG("validating chunklist signature against pub keys"); + for (size_t i = 0; i < CHUNKLIST_NPUBKEYS; i++) { + const struct chunklist_pubkey *key = &chunklist_pubkeys[i]; + err = validate_signature(key->key, CHUNKLIST_PUBKEY_LEN, + buf + hdr->cl_sig_offset, sigsz, sha_digest); + if (err == 0) { + AUTHDBG("validated chunklist signature with key %lu (prod=%d)", i, key->isprod); + valid_sig = key->isprod; +#if IMAGEBOOT_ALLOW_DEVKEYS + if (!key->isprod) { + /* allow dev keys in dev builds only */ + AUTHDBG("*** allowing DEV key: this will fail in customer builds ***"); + valid_sig = true; + } +#endif + goto out; + } else if (err == INVALID_SIG) { + /* try the next key */ + } else { + goto out; /* something bad happened */ + } + } + + /* At this point we tried all the keys: nothing went wrong but none of them + * signed our chunklist. */ + AUTHPRNT("signature did not verify against any known public key"); + +out: + if (err) { + return err; + } else if (valid_sig == true) { + return 0; /* signed, and everything checked out */ + } else { + return EINVAL; + } +} + +static int +validate_root_image(const char *root_path, void *chunklist) +{ + int err = 0; + struct chunklist_hdr *hdr = chunklist; + struct chunklist_chunk *chk = NULL; + size_t ch = 0; + struct nameidata ndp = {}; + struct vnode *vp = NULL; + off_t fsize = 0; + off_t offset = 0; + bool doclose = false; + size_t bufsz = 0; + void *buf = NULL; + + vfs_context_t ctx = vfs_context_kernel(); + kauth_cred_t kerncred = vfs_context_ucred(ctx); + proc_t p = vfs_context_proc(ctx); + + AUTHDBG("validating root dmg %s", root_path); + + /* + * Open the DMG + */ + NDINIT(&ndp, LOOKUP, OP_OPEN, LOCKLEAF, UIO_SYSSPACE, root_path, ctx); + if ((err = namei(&ndp)) != 0) { + AUTHPRNT("namei failed (%s)", root_path); + goto out; + } + nameidone(&ndp); + vp = ndp.ni_vp; + + if (vp->v_type != VREG) { + err = EINVAL; + goto out; + } + + if ((err = vnode_size(vp, &fsize, ctx)) != 0) { + AUTHPRNT("failed to get vnode size"); + goto out; + } + + if ((err = VNOP_OPEN(vp, FREAD, ctx)) != 0) { + AUTHPRNT("failed to open vnode"); + goto out; + } + doclose = true; + + /* + * Iterate the chunk list and check each chunk + */ + chk = chunklist + hdr->cl_chunk_offset; + for (ch = 0; ch < hdr->cl_chunk_count; ch++) { + int resid = 0; + + if (!buf) { + /* allocate buffer based on first chunk size */ + buf = kalloc(chk->chunk_size); + if (buf == NULL) { + err = ENOMEM; + goto out; + } + bufsz = chk->chunk_size; + } + + if (chk->chunk_size > bufsz) { + AUTHPRNT("chunk size too big"); + err = EINVAL; + goto out; + } + + err = vn_rdwr(UIO_READ, vp, (caddr_t)buf, chk->chunk_size, offset, UIO_SYSSPACE, IO_NODELOCKED, kerncred, &resid, p); + if (err) { + AUTHPRNT("vn_rdrw fail (err = %d, resid = %d)", err, resid); + goto out; + } + if (resid) { + err = EINVAL; + AUTHPRNT("chunk covered non-existant part of image"); + goto out; + } + + /* calculate the SHA256 of this chunk */ + uint8_t sha_digest[SHA256_DIGEST_LENGTH]; + SHA256_CTX sha_ctx; + SHA256_Init(&sha_ctx); + SHA256_Update(&sha_ctx, buf, chk->chunk_size); + SHA256_Final(sha_digest, &sha_ctx); + + /* Check the calculated SHA matches the chunk list */ + if (bcmp(sha_digest, chk->chunk_sha256, SHA256_DIGEST_LENGTH) != 0) { + AUTHPRNT("SHA mismatch on chunk %lu (offset %lld, size %u)", ch, offset, chk->chunk_size); + err = EINVAL; + goto out; + } + + if (os_add_overflow(offset, chk->chunk_size, &offset)) { + err = EINVAL; + goto out; + } + chk++; + } + + if (offset != fsize) { + AUTHPRNT("chunklist did not cover entire file (offset = %lld, fsize = %lld)", offset, fsize); + err = EINVAL; + goto out; + } + +out: + kfree_safe(buf); + if (doclose) { + VNOP_CLOSE(vp, FREAD, ctx); + } + if (vp) { + vnode_put(vp); + vp = NULL; + } + + return err; +} + +static int +construct_chunklist_path(const char *root_path, char **bufp) +{ + int err = 0; + char *path = NULL; + size_t len = 0; + + path = kalloc(MAXPATHLEN); + if (path == NULL) { + AUTHPRNT("failed to allocate space for chunklist path"); + err = ENOMEM; + goto out; + } + + len = strnlen(root_path, MAXPATHLEN); + if (len < MAXPATHLEN && len > strlen(".dmg")) { + /* correctly terminated string with space for extension */ + } else { + AUTHPRNT("malformed root path"); + err = EINVAL; + goto out; + } + + len = strlcpy(path, root_path, MAXPATHLEN); + if (len >= MAXPATHLEN) { + AUTHPRNT("root path is too long"); + err = EINVAL; + goto out; + } + + path[len - strlen(".dmg")] = '\0'; + len = strlcat(path, ".chunklist", MAXPATHLEN); + if (len >= MAXPATHLEN) { + AUTHPRNT("chunklist path is too long"); + err = EINVAL; + goto out; + } + +out: + if (err) { + kfree_safe(path); + } else { + *bufp = path; + } + return err; +} + +static int +authenticate_root(const char *root_path) +{ + char *chunklist_path = NULL; + void *chunklist_buf = NULL; + size_t chunklist_len = 32*1024*1024UL; + int err = 0; + + err = construct_chunklist_path(root_path, &chunklist_path); + if (err) { + AUTHPRNT("failed creating chunklist path"); + goto out; + } + + AUTHDBG("validating root against chunklist %s", chunklist_path); + + /* + * Read and authenticate the chunklist, then validate the root image against + * the chunklist. + */ + + AUTHDBG("reading chunklist"); + err = read_file(chunklist_path, &chunklist_buf, &chunklist_len); + if (err) { + AUTHPRNT("failed to read chunklist"); + goto out; + } + + AUTHDBG("validating chunklist"); + err = validate_chunklist(chunklist_buf, chunklist_len); + if (err < 0) { + AUTHDBG("missing or incorrect signature on chunklist"); + goto out; + } else if (err) { + AUTHPRNT("failed to validate chunklist"); + goto out; + } else { + AUTHDBG("successfully validated chunklist"); + } + + AUTHDBG("validating root image against chunklist"); + err = validate_root_image(root_path, chunklist_buf); + if (err) { + AUTHPRNT("failed to validate root image against chunklist (%d)", err); + goto out; + } + + /* everything checked out - go ahead and mount this */ + AUTHDBG("root image authenticated"); + + out: + kfree_safe(chunklist_buf); + kfree_safe(chunklist_path); + return err; +} + +static const uuid_t * +getuuidfromheader_safe(const void *buf, size_t bufsz, size_t *uuidsz) +{ + const struct uuid_command *cmd = NULL; + const kernel_mach_header_t *mh = buf; + + /* space for the header and at least one load command? */ + if (bufsz < sizeof(kernel_mach_header_t) + sizeof(struct uuid_command)) { + AUTHPRNT("libkern image too small"); + return NULL; + } + + /* validate the mach header */ + if (mh->magic != MH_MAGIC_64 || (mh->sizeofcmds > bufsz - sizeof(kernel_mach_header_t))) { + AUTHPRNT("invalid MachO header"); + return NULL; + } + + /* iterate the load commands */ + size_t offset = sizeof(kernel_mach_header_t); + for (size_t i = 0; i < mh->ncmds; i++) { + cmd = buf + offset; + + if (cmd->cmd == LC_UUID) { + *uuidsz = sizeof(cmd->uuid); + return &cmd->uuid; + } + + if (os_add_overflow(cmd->cmdsize, offset, &offset) || + offset > bufsz - sizeof(struct uuid_command)) { + return NULL; + } + } + + return NULL; +} + +static const char *libkern_path = "/System/Library/Extensions/System.kext/PlugIns/Libkern.kext/Libkern"; +static const char *libkern_bundle = "com.apple.kpi.libkern"; + +/* + * Check that the UUID of the libkern currently loaded matches the one on disk. + */ +static int +auth_version_check(void) +{ + int err = 0; + void *buf = NULL; + size_t bufsz = 4*1024*1024UL; + + /* get the UUID of the libkern in /S/L/E */ + + err = read_file(libkern_path, &buf, &bufsz); + if (err) { + goto out; + } + + unsigned long uuidsz = 0; + const uuid_t *img_uuid = getuuidfromheader_safe(buf, bufsz, &uuidsz); + if (img_uuid == NULL || uuidsz != sizeof(uuid_t)) { + AUTHPRNT("invalid UUID (sz = %lu)", uuidsz); + err = EINVAL; + goto out; + } + + /* Get the UUID of the loaded libkern */ + uuid_t live_uuid; + err = OSKextGetUUIDForName(libkern_bundle, live_uuid); + if (err) { + AUTHPRNT("could not find loaded libkern"); + goto out; + } + + /* ... and compare them */ + if (bcmp(live_uuid, img_uuid, uuidsz) != 0) { + AUTHPRNT("UUID of running libkern does not match %s", libkern_path); + + uuid_string_t img_uuid_str, live_uuid_str; + uuid_unparse(*img_uuid, img_uuid_str); + uuid_unparse(live_uuid, live_uuid_str); + AUTHPRNT("loaded libkern UUID = %s", live_uuid_str); + AUTHPRNT("on-disk libkern UUID = %s", img_uuid_str); + + err = EINVAL; + goto out; + } + + /* UUID matches! */ + +out: + kfree_safe(buf); + return err; +} + +#if 0 +int +auth_imgboot_test(proc_t __unused ap, struct auth_imgboot_test_args *uap, int32_t *retval) +{ + int ret = 0; + int err; + char path[MAXPATHLEN]; + vm_size_t len; + *retval = 0; + + err = copyinstr(uap->path, path, MAXPATHLEN, &len); + if (err) { + return err; + } + if (len >= MAXPATHLEN) { + return ENAMETOOLONG; + } + + AUTHDBG("authenticating root image at %s", path); + err = authenticate_root(path); + if (err) { + AUTHPRNT("root authentication FAIL (%d)", err); + ret = err; + } else { + AUTHDBG("successfully authenticated %s", path); + } + + AUTHDBG("checking root image version"); + err = auth_version_check(); + if (err) { + AUTHPRNT("root image version check FAIL (%d)", err); + err = err ?: ret; + } else { + AUTHPRNT("root version check success (%d)", err); + } + + if (ret < 0) { + return EINVAL; /* negative return values have special meaning */ + } + return ret; +} +#endif + +static boolean_t imageboot_setup_new() { int error; char *root_path = NULL; int height = 0; boolean_t done = FALSE; + boolean_t auth_root = FALSE; MALLOC_ZONE(root_path, caddr_t, MAXPATHLEN, M_NAMEI, M_WAITOK); assert(root_path != NULL); - if(PE_parse_boot_argn(IMAGEBOOT_CONTAINER_ARG, root_path, MAXPATHLEN) == TRUE) { + if (PE_parse_boot_argn(IMAGEBOOT_CONTAINER_ARG, root_path, MAXPATHLEN) == TRUE) { printf("%s: container image url is %s\n", __FUNCTION__, root_path); error = imageboot_mount_image(root_path, height); if (error != 0) { @@ -223,21 +853,67 @@ imageboot_setup_new() height++; } - if (PE_parse_boot_argn(IMAGEBOOT_ROOT_ARG, root_path, MAXPATHLEN) == FALSE) { + if (PE_parse_boot_argn(IMAGEBOOT_AUTHROOT_ARG, root_path, MAXPATHLEN) == TRUE) { + auth_root = TRUE; + } else if (PE_parse_boot_argn(IMAGEBOOT_ROOT_ARG, root_path, MAXPATHLEN) == FALSE) { if (height > 0) { panic("%s specified without %s?\n", IMAGEBOOT_CONTAINER_ARG, IMAGEBOOT_ROOT_ARG); } goto out; + } + + printf("%s: root image url is %s\n", __func__, root_path); +#if CONFIG_CSR + if (auth_root && (csr_check(CSR_ALLOW_ANY_RECOVERY_OS) == 0)) { + AUTHPRNT("CSR_ALLOW_ANY_RECOVERY_OS set, skipping root image authentication"); + auth_root = false; } +#endif - printf("%s: root image url is %s\n", __FUNCTION__, root_path); + if (auth_root) { + /* Copy the path to use locally */ + char *path_alloc = kalloc(MAXPATHLEN); + if (path_alloc == NULL) { + panic("imageboot path allocation failed\n"); + } + + char *path = path_alloc; + strlcpy(path, root_path, MAXPATHLEN); + + size_t len = strlen(kIBFilePrefix); + if (strncmp(kIBFilePrefix, path, len) == 0) { + /* its a URL - remove the file:// prefix and percent-decode */ + path += len; + url_decode(path); + } + + AUTHDBG("authenticating root image at %s", path); + error = authenticate_root(path); + if (error) { + panic("root image authentication failed (err = %d)\n", error); + } + AUTHDBG("successfully authenticated %s", path); + + kfree_safe(path_alloc); + } error = imageboot_mount_image(root_path, height); if (error != 0) { panic("Failed to mount root image."); } + if (auth_root) { + /* check that the image version matches the running kernel */ + AUTHDBG("checking root image version"); + error = auth_version_check(); + if (error) { + panic("root image version check failed"); + } else { + AUTHDBG("root image version matches kernel"); + } + } + done = TRUE; out: @@ -253,19 +929,20 @@ imageboot_setup() DBG_TRACE("%s: entry\n", __FUNCTION__); - if (rootvnode == NULL) { + if (rootvnode == NULL) { panic("imageboot_setup: rootvnode is NULL."); } /* * New boot-arg scheme: * root-dmg : the dmg that will be the root filesystem. + * auth-root-dmg : same as root-dmg but with image authentication. * container-dmg : an optional dmg that contains the root-dmg. */ if (imageboot_setup_new()) { return; } - + MALLOC_ZONE(root_path, caddr_t, MAXPATHLEN, M_NAMEI, M_WAITOK); assert(root_path != NULL);