Demonstrate pointer revocation
Indirect control flow through aliased heap objects
This exercise demonstrates CheriBSD's pointer revocation facility and its use
by the system malloc. It asks you to contrast the same program,
temporal-control.c, built and run in three slightly different environments.
It must be run on a heap-temporal-safety enabled version of CheriBSD; at the
time of writing, heap temporal safety remains an experimental feature not yet
merged to mainline CheriBSD.
- Compile
temporal-control.cwith a RISC-V target and a binary name oftemporal-control-riscv.
temporal-control.c
/*
* SPDX-License-Identifier: BSD-2-Clause
* Copyright (c) 2020 Microsoft, Inc.
*/
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
/* Ensure we're being run on a temporal-safety-aware system */
#ifdef __CHERI_PURE_CAPABILITY__
#include <cheri/revoke.h>
__attribute__((used))
static void *check_cheri_revoke = cheri_revoke;
extern void malloc_revoke(void);
__attribute__((used))
static void *check_malloc_revoke = malloc_revoke;
#endif
static void
fn1(uintptr_t arg)
{
fprintf(stderr, " First function: %#p\n", (void *)arg);
}
static void
fn2(uintptr_t arg)
{
fprintf(stderr, " Second function: %#p\n", (void *)arg);
}
struct obj {
char buf[32];
/*
* The following are marked volatile to ensure the compiler doesn't
* constant propagate fn (making aliasing not work) and to ensure
* neither stores to them are optimised away entirely as dead due
* to calling free.
*/
void (* volatile fn)(uintptr_t);
volatile uintptr_t arg;
};
int
main(void)
{
struct obj * volatile obj1 = calloc(1, sizeof(*obj1));
fprintf(stderr, "Installing function pointer in obj1 at %#p\n", obj1);
obj1->fn = fn1;
obj1->arg = (uintptr_t)obj1;
free(obj1);
fprintf(stderr, "Demonstrating use after free:\n");
obj1->fn(obj1->arg);
#ifdef CAPREVOKE
/* Force recycling the free queue now, but with a revocation pass */
malloc_revoke();
#endif
struct obj * volatile obj2 = malloc(sizeof(*obj2));
#ifdef CAPREVOKE
assert(obj1 == obj2);
#endif
fprintf(stderr, "Assigning function pointer through obj2 at %#p\n",
obj2);
obj2->fn = fn2;
fprintf(stderr, "Calling function pointer through obj1 (now %#p):\n",
obj1);
obj1->fn(obj1->arg);
return (0);
}
- Run the resulting program and observe that the system malloc has reused
a location on the heap, such that
obj1andobj2point to the same address. Moreover, the assignment offn2intoobj2causes the last printout to be fromfn2, notfn1, even though the function pointer was fetched throughobj1andobj1->fnwas last set tofn1. - Recompile
temporal-control.cwith a CHERI-RISC-V target and binary name oftemporal-control-cheri. - Run this program instead. Why does it no longer exhibit the behavior from step 2? Ponder the suitability of using just this approach for fixing temporal aliasing.
- Recompile
temporal-control.c, adding-DCAPREVOKEto the command line this time, with a CHERI-RISC-V target and a binary name oftemporal-control-cheri-revoke. - Run this third program instead and note that it crashes, catching a
SIGPROTbetween declaring its intent to callobj1->fnand declaring that it has made the call. Can you spot why it has crashed? - Rerun the third program under
gdband look at both the instruction triggering theSIGPROTand the register(s) involved. Why is the program crashing? What must have happened while the system was executing the mysteriousmalloc_revoke()function? - Modify
temporal-control.cto try to induce aliasing by making many allocations: callmallocandfreerepeatedly until the new allocation compares equal toobj1. Ah ha, you've caught the allocator now! But wait, what isobj1in full (i.e., as a capability, not merely a virtual address)? You likely have to callfreein the loop for this exercise to work; merely callingmallocmay instead simply always return new addresses, even if the initialobj1has beenfree-d.
More attacks through aliased heap objects
The program is called temporal-control.c because it exhibits temporal
aliasing of heap pointers and because the class of bugs it mimics involve
transfers of control through function pointers held in heap objects. While
CHERI protects against pointer injection, it cannot so easily defend against
either:
- capability farming: as in the example, a legitimately-held capability can be (caused to be) stored to a "new" heap object, altering an aliased view while preserving the set tag bit; or
- data-based corruption through temporal aliasing.
These windows open wider considering that, unlike this example, temporal aliasing often comes paired with type-confusion, so it may be possible to overlap an easily-controlled structure with an exploitable one.
- Write a program like
temporal-control.cin which changing a data byte within a temporally-aliased heap object suffices to cause the program to error. Perhaps the heap object is the state associated with a client session and contains a flag that indicates superuser status. - Demonstrate that this program fails as expected on RISC-V but that any attempt to induce aliasing is thwarted on CHERI-RISC-V with heap temporal safety: aliasing becomes possible only after revocation, ensuring that attempts to use the old session object fail-stop.