CheriABI Showcase
This exercise demonstrates several aspects of CheriABI, which defines how CheriBSD processes are constructed, how function calls are made, how a process interacts with the kernel through system calls, and other such foundational details.
The Kernel Voluntarily Honors Capability Bounds
kern-read-over.c
demonstrates a (potential) loss of spatial safety when
pointers are passed from userspace to the kernel. The kernel, by convention,
has full access to all of userspace memory. Even when CheriBSD is running
CheriABI programs, this is true: the kernel holds a capability with full RWX
access to all userspace addresses. Therefore, the kernel can act as a
confused deputy, accessing memory with its legitimate authority but without
intent.
-
Compile
kern-read-over.c
for both the baseline (kern-read-over-baseline
) and CHERI-enabled (kern-read-over-cheri
) architectures. -
Run these programs and observe their outputs.
-
Focusing on the
read()
system call, what happens in the two versions of the program. When, in particular, does it look like the CHERI version notices something is amiss? -
If you have done the inter-stack-object buffer overflow exercise, contrast the behaviors of the two CHERI-enabled programs.
The Process Memory Map
In most UNIX programs, the rights to manipulate the virtual memory map are
ambient: any piece of code can change the virtual memory permissions
associated with a page, munmap
pages, or even request a replacement mapping
("fixed mmap
") almost anywhere in the address space. This risks allowing
evasion of CHERI capabilities' protection properties, as CHERI capabilities are
interpreted in combination with the virtual memory map.
Therefore, the CheriBSD kernel avails itself of a software permission bit in
CHERI capabilities. Such permissions are not architecturally interpreted but
are still subject to architectural protection (and so, for example, a zero
permission bit may not be set to one without simultaneously clearing the
capability tag). In particular, CheriBSD defines CHERI_PERM_SW_VMEM
, sets
this permission bit when returning pointers to new allocations of address
space, and requires that capabilities passed to address-space-manipulating
functions bear this permission. Userspace components are free to clear this
permission when delegating access to address space.
-
Compile
perm-vmem.c
for both the baseline (perm-vmem-baseline
) and CHERI-enabled (perm-vmem-cheri
) architectures. -
Run these programs and observe their outputs. The
printf
format strings for capabilities,%p
and%#p
, elide some usually-excessive details, andCHERI_PERM_SW_VMEM
is generally regarded as one such.gdb
's pretty-printing, similarly. However, we can programmatically extract the permissions field and display it. -
Modify
perm-vmem.c
to verify thatmadvise(MADV_FREE)
andmmap(MAP_FIXED)
also are permitted for the capability returned directly frommmap
but are not permitted for the heap-derived pointer.
(Extra Credit!) Initial Process Construction
We have largely focused on program behavior after it has been loaded and is running. Let us look in a little more detail at some aspects of the initial construction. While modern ELF loading is well beyond the scope of this document, and is perhaps best summarized as "here be dragons", we can nevertheless take a quick glance at some interesting features of CheriABI startup.
-
Compile
print-more.c
for both the baseline (print-more-baseline
) and the CHERI-enabled (print-more-cheri
) architectures. -
Run both these programs and observe their outputs. As might be predicted, the CHERI version reports a wide variety of capabilities to different parts of the address space. Run both programs several times; what do you observe?
Let us examine several interesting aspects of the reported capabilities.
-
Launch
gdb ./print-more-cheri
and have it start the program and stop before running any instructions, withstarti
. Where do we find ourselves? -
Use
info inferiors
to obtain the child process identifier (PID) and!procstat vm NNN
(replacingNNN
with the child PID) to show the initial address space arranged by the kernel.Which of these initial mappings are targeted by the values reported for
&rodata_const
,&relro_ptr
,&rw_ptr
andprintf
in step 2? What are the permissions for these mappings? -
Just because the page mappings exist, however, CHERI programs need to have capabilities installed to access them. Here at the very beginning of a process's life, we are in a good position to see the root capabilities that the kernel makes available. Use
info registers
to see the initial contents of the register file. -
Let's begin our tour with
csp
, the capability stack pointer register.First, what may strike you as surprising (and why) about the stack pointer being replaced by a capability?
Second, compare the address space map obtained above with the current
csp
value; what has the kernel arranged to "back" the region of address space within stack bounds?If you are familiar with Stack Clash Vulnerabilities, explain how the two aspects above work in tandem to mitigate this class of vulnerability.
Third, contrast the relative order of
&argv[0]
and&stack_local
as reported on the two different architectures in step 2 above. -
Having access to the stack is all well and good, but surely there is more to a process than that. At the beginning of a CheriABI process's life, the capability in
ca0
(the first "argument register") points to the "auxiliary vector", an array ofElf_Auxinfo
structures constructed by the kernel.gdb
can ask the kernel for, and display, the information in the auxiliary vector withinfo auxv
. However, the pretty-printer is not capability aware, so let's also directly spelunk the structure. Use somegdb
scripting to print out the auxiliary vector in its entirety:set $i = 0 while(((Elf_Auxinfo *)$ca0)[$i].a_type != 0) p ((Elf_Auxinfo *)$ca0)[$i] set $i = $i + 1 end
Use the more human-friendly
info auxv
to interpret thea_type
values.In addition to the
AT_ARGV
value we have already (indirectly) seen above, there are many other capabilities to nearby parts of the address space, including the initial environment vector (AT_ENVV
) and the executable path (AT_EXECPATH
).More usefully, however,
-
AT_PHDR
supplies a read/write capability to the loaded executable. -
AT_ENTRY
supplies a read/execute capability to the loaded executable, pointed at its entrypoint. -
AT_BASE
supplies a full read/write/execute capability to the program's "interpreter" (dynamic loader). The elevated permissions here allow the loader to (relatively) easily relocate itself early in execution.
From which of these capabilities are the displayed values of
&rodata_const
,&relro_ptr
, and&rw_ptr
from step 2 sourced? What permissions have been shed in the derivation? How do these permissions differ from those of the underlying page mappings? -
-
The displayed value for
printf
is tagged as being a(sentry)
. Modify the program to attempt to display the result of computing either*(char *)(printf)
or(void*)((uintptr_t)printf + 1)
.
Compile and run this modified version (or both). What happens?
Sentry (short for "Sealed Entry") capabilities are a special form of capabilities: they are immutable and inert, conveying to the bearer no authority to the target, until they become the program counter, at which point they are unsealed into being an ordinary capability. Thus, we can neither read through nor mutate our handle to
printf
, yet we can jump to it.If you are familiar with Return Oriented Programming and Jump Oriented Programming, you may wish to consider the cumulative challenge added by CHERI's architectural provenance requirement combined with pervasive use of sentry capabilities for dynamically resolved symbols.
Source
kern-read-over.c
/*
* SPDX-License-Identifier: BSD-2-Clause-DARPA-SSITH-ECATS-HR0011-18-C-0016
* Copyright (c) 2020 SRI International
* Copyright (c) 2022 Microsoft Corporation
*/
#include <assert.h>
#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define bsz 16
int
main(void)
{
int fds[2];
pid_t pid;
if (pipe(fds) == -1)
err(1, "pipe");
if ((pid = fork()) == -1)
err(1, "fork");
if (pid == 0) {
char out[2*bsz];
for (size_t i = 0; i < sizeof(out); i++) {
out[i] = 0x10 + i;
}
if (write(fds[0], out, sizeof(out)) != sizeof(out)) {
err(1, "write");
}
printf("Write OK\n");
} else {
int res;
char upper[bsz] = { 0 };
char lower[bsz] = { 0 };
waitpid(pid, NULL, 0);
printf("lower=%p upper=%p\n", lower, upper);
assert((ptraddr_t)upper == (ptraddr_t)&lower[sizeof(lower)]);
res = read(fds[1], lower, sizeof(lower) + sizeof(upper));
assert(res != 0);
if (res > 0) {
printf("Read 0x%x OK; lower[0]=0x%x upper[0]=0x%x\n",
res, lower[0], upper[0]);
} else if (res < 0) {
printf("Bad read (%s); lower[0]=0x%x upper[0]=0x%x\n",
strerror(errno), lower[0], upper[0]);
}
}
return 0;
}
perm-vmem.c
/*
* SPDX-License-Identifier: BSD-2-Clause
* Copyright (c) 2022 Microsoft Corporation
*/
#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#ifdef __CHERI_PURE_CAPABILITY__
#include <cheri/cherireg.h>
#define PRINTF_PTR "#p"
#else
#define PRINTF_PTR "p"
#endif
int
main(void)
{
char *m, *p;
int res;
/* Get a page from the kernel and give it back */
p = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANON,
-1, 0);
assert(p != MAP_FAILED);
printf("Directly mapped page at p=%" PRINTF_PTR "\n", p);
#ifdef __CHERI_PURE_CAPABILITY__
printf(" p.perms=0x%lx\n", __builtin_cheri_perms_get(p));
#endif
res = madvise(p, 4096, MADV_FREE);
assert(res == 0);
p = mmap(p, 4096, PROT_READ|PROT_WRITE, MAP_FIXED | MAP_PRIVATE | MAP_ANON,
-1, 0);
assert(p != MAP_FAILED);
res = munmap(p, 4096);
assert(res == 0);
/* Get a pointer to a whole page of the heap*/
m = malloc(8192);
p = __builtin_align_up(m, 4096);
printf("Punching hole in the heap at p=%" PRINTF_PTR "\n", p);
#ifdef __CHERI_PURE_CAPABILITY__
printf(" p.perms=0x%lx\n", __builtin_cheri_perms_get(p));
#endif
char *q = mmap(p, 4096, PROT_READ|PROT_WRITE, MAP_FIXED | MAP_PRIVATE | MAP_ANON,
-1, 0);
assert(q != MAP_FAILED);
if (madvise(p, 4096, MADV_FREE) != 0) {
printf("madvise failed: %s\n", strerror(errno));
}
if (munmap(p, 4096) != 0) {
printf("munmap failed: %s\n", strerror(errno));
}
printf("Done\n");
}
print-more.c
/*
* SPDX-License-Identifier: BSD-2-Clause
* Copyright (c) 2022 Microsoft Corporation
*/
#include <assert.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#ifdef __CHERI_PURE_CAPABILITY__
#define PRINTF_PTR "#p"
#else
#define PRINTF_PTR "p"
#endif
static const int rodata_const = 42;
static int (*const relro_ptr)(const char *, ...) = printf;
static int (*rw_ptr)(const char *, ...) = printf;
int
main(int argc, char **argv)
{
int stack_local;
printf("&argv[0]=%" PRINTF_PTR "\n", &argv[0]);
printf(" argv[0]=%" PRINTF_PTR "\n", argv[0]);
printf("&stack_local=%" PRINTF_PTR "\n", &stack_local);
printf("&rodata_const=%" PRINTF_PTR "\n", &rodata_const);
printf("&relro_ptr=%" PRINTF_PTR "\n", &relro_ptr);
printf("&rw_ptr=%" PRINTF_PTR "\n", &rw_ptr);
printf("printf=%" PRINTF_PTR "\n", printf);
}
Courseware
This exercise has presentation materials available.