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.c
with 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
obj1
andobj2
point to the same address. Moreover, the assignment offn2
intoobj2
causes the last printout to be fromfn2
, notfn1
, even though the function pointer was fetched throughobj1
andobj1->fn
was last set tofn1
. - Recompile
temporal-control.c
with 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-DCAPREVOKE
to 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
SIGPROT
between declaring its intent to callobj1->fn
and declaring that it has made the call. Can you spot why it has crashed? - Rerun the third program under
gdb
and look at both the instruction triggering theSIGPROT
and 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.c
to try to induce aliasing by making many allocations: callmalloc
andfree
repeatedly until the new allocation compares equal toobj1
. Ah ha, you've caught the allocator now! But wait, what isobj1
in full (i.e., as a capability, not merely a virtual address)? You likely have to callfree
in the loop for this exercise to work; merely callingmalloc
may instead simply always return new addresses, even if the initialobj1
has 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.c
in 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.