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.cfor 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.cfor both the baseline (perm-vmem-baseline) and CHERI-enabled (perm-vmem-cheri) architectures. -
Run these programs and observe their outputs. The
printfformat strings for capabilities,%pand%#p, elide some usually-excessive details, andCHERI_PERM_SW_VMEMis generally regarded as one such.gdb's pretty-printing, similarly. However, we can programmatically extract the permissions field and display it. -
Modify
perm-vmem.cto verify thatmadvise(MADV_FREE)andmmap(MAP_FIXED)also are permitted for the capability returned directly frommmapbut 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.cfor 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-cheriand have it start the program and stop before running any instructions, withstarti. Where do we find ourselves? -
Use
info inferiorsto obtain the child process identifier (PID) and!procstat vm NNN(replacingNNNwith 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_ptrandprintfin 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 registersto 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
cspvalue; 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_localas 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_Auxinfostructures constructed by the kernel.gdbcan 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 somegdbscripting 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 endUse the more human-friendly
info auxvto interpret thea_typevalues.In addition to the
AT_ARGVvalue 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_PHDRsupplies a read/write capability to the loaded executable. -
AT_ENTRYsupplies a read/execute capability to the loaded executable, pointed at its entrypoint. -
AT_BASEsupplies 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_ptrfrom 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
printfis 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.