Exploiting heap use-after-free to manipulate control flow
This mission requires a CheriBSD sysroot and image with temporal safety! The executable can be built for both RISC-V and CHERI-RISC-V (and exploring both may be worthwhile), but the mission is for CHERI-RISC-V with heap temporal safety enforcement in place.
This mission is a potted exercise inspired by real-world vulnerabilities where
an active adversary can influence the contents of a server's heap and, often
without completing authentication, walk the server's protocol state machine
through erroneous states. Popular bugs facilitating these exploits include
double free, use-after-reallocation, and type confusion (esp. of temporally
aliased objects). For this mission, we have simplified the interface to be a
little "command language" read over stdin
, detailed below.
The success criterion is executing the success
function. To ease testing,
this is the only case in which the program source claims to exit with a code of
42
. Ordinary termination is signaled with 0
and invalid input results in
the program terminating with 1
. While the source does not overtly claim any
other outcome, if control flow can be redirected, almost anything goes.
Program Overview
This is a (very) minimal simulation of a program that manages multiple
sessions, objects within each session, and limited information flow between
those sessions. Rather than network sockets, each session is represented by a
struct farm
and there are up to four such active at any moment. Within each
struct farm
is a circular collection of struct crop
s, at most one of which
may be selected by the cursor
of its owning struct farm
. If no crop is
selected, the cursor is said to be "in the gap". The crop at the cursor, if
any, can be asked to describe itself, using a function pointer within the
struct crop
itself.
To make matters more exciting, a UFO may be summoned to perform particular
kinds of mischief. The UFO may "abduct" a pointer into itself and can create
crop signs within struct crop
(with data) or struct farm
(with a capability
to the success()
function).
The gadgets offered by the UFO are somewhat limited (that is, they are not
"write what where" or arbitrary control transfer) as one might expect to see in
a real application. Nevertheless, they are sufficiently powerful to provide
myriad exploitation vectors on RISC-V without CHERI, and even a few on
CHERI-RISC-V. However, we believe that enforced heap temporal safety will
ensure that any use of them is either overwritten by subsequent program
operations before the gadgets' effects influence control flow or will cause the
program to fail-stop with a default-fatal signal (e.g., SIGSEGV
or
SIGPROT
).
Building and running
We suggest using the ccc
tool provided with this book, building for
riscv64-purecap
. For experimentation, this program also builds for
riscv64
, without CHERI. On riscv64-purecap
builds, heap temporal safety
enforcement may be disabled at program load time by setting the environment
variable MALLOC_DISABLE_REVOCATION
to a non-empty value. (While setting this
flag prior to program loading disqualifies such invocations from completing the
mission, being able to disable revocation from within main()
would be quite
interesting indeed.)
As said before, the program expects to read a command language from stdin
.
It will print "Ready" and then await input. Unlike real applications, this
program is fairly chatty about its own operation to ease exploration.
Nevertheless, it may be useful to run it within gdb
, especially to
differentiate causes of crashes.
Command Language Directives
-
Whitespace is quietly ignored in most cases; this may simplify reading programs.
-
Digits
0
through3
focus on the corresponding farm slots, making it the locus of subsequent commands until altered. -
F
allocates a new farm at the current slot. -
f
frees the current slot's farm (along with any crops in its collection) -
C
creates a new crop in the current farm and puts it at the left of the collection. The cursor is left in the gap or pointing at the crop it was before. If the cursor is not in the gap, the new crop will inherit the description of the selected crop; otherwise, the program chooses one of two varieties of cherry. -
L
andR
move the current farm's cursor left and right, respectively. The farm's collection is circular with a gap; moving left (right) from the gap places the cursor at the rightmost (leftmost) element, and walking off either end returns the cursor to the gap. -
Z
moves the current farm's cursor to the gap. -
D
describes the crop at the current farm's cursor, if that cursor is not in the gap. -
c
removes the crop at the current farm's cursor from the collection and frees it. -
U
causes a UFO to arrive;u
causes it to leave. There is at most one UFO at any moment. -
A
causes the UFO, if present, to abduct the current farm's cursor, presumably for further scrutiny. -
S
causes the UFO to make a crop sign on the current farm;s
causes the UFO to read the nextsizeof(void*)
characters fromstdin
(whitespace is not ignored for this) and use that to sign the crop indicated by the current farm's cursor (a smaller crop sign, if you will). Writing crop signs of either variety is very destabilizing and likely to lead to crashes! -
On CHERI-RISC-V builds running with heap temporal safety enforcement,
!
will force a revocation pass, destroying pointers to free objects and allowing reuse of memory and address space.
Example Session
Here is a short session which creates one struct farm
, two struct crop
s,
exhibits cursor control and description of struct crop
s, and then tears down
the struct farm
:
$ echo FCC RD ZLD f | ./temporal-mission
Ready (CHERI-RISC-V)
New farm (index 0) at 0x41201080
New crop at 0x41201100
New crop at 0x41201180
Farm 0 cursor 0x41201180
FYI: Current farm is 0x41201080
FYI: cursor 0x41201180
FYI: cursor->describe 0x102fd2
Chelan at 0x41201180
Farm 0 cursor 0x41201100
FYI: Current farm is 0x41201080
FYI: cursor 0x41201100
FYI: cursor->describe 0x10300e
Colt at 0x41201100
Tear down farm (index 0) at 0x41201080
Source code
temporal-mission.c
/*
* SPDX-License-Identifier: BSD-2-Clause
* Copyright (c) 2020 Microsoft, Inc.
*/
#include <assert.h>
#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/queue.h>
#if defined(__CHERI_PURE_CAPABILITY__)
#include <cheri/revoke.h>
static void __attribute__((used)) *check_cheri_revoke = cheri_revoke;
extern void malloc_revoke(void);
static void __attribute__((used)) *check_malloc_revoke = malloc_revoke;
#endif
struct farm;
struct crop;
static void
success(struct farm *f)
{
fprintf(stderr, "Exploit successful: computer bought the farm!\n");
exit(42);
}
TAILQ_HEAD(cropq, crop);
struct farm {
struct cropq cropq;
struct crop *cursor;
};
struct crop {
void (*describe)(struct crop *);
TAILQ_ENTRY(crop) farm_cropq;
};
_Static_assert(sizeof(struct crop) == sizeof(struct farm),
"Structure size mismatch");
union ufo {
void *ptrA;
char buf[sizeof(struct crop)];
};
_Static_assert(sizeof(struct crop) == sizeof(union ufo),
"Structure size mismatch");
static void
descr_1(struct crop *c)
{
fprintf(stderr, "Chelan at %p\n", c); /* Pacific Northwest */
}
static void
descr_2(struct crop *c)
{
fprintf(stderr, "Colt at %p\n", c); /* United Kingdom */
}
static unsigned int cid;
static union ufo *ufo;
#define NFARM 4
struct farm *farmp[NFARM];
static void
rm_farm(int fix)
{
struct farm *f = farmp[fix];
farmp[fix] = NULL;
if (f != NULL) {
struct crop *c, *tc;
TAILQ_FOREACH_SAFE(c, &f->cropq, farm_cropq, tc) {
TAILQ_REMOVE(&f->cropq, c, farm_cropq);
free(c);
}
fprintf(stderr, "Tear down farm (index %d) at %p\n", fix, f);
free(f);
}
}
static struct farm *
mk_farm(int fix)
{
struct farm *f;
rm_farm(fix);
f = malloc(sizeof(struct farm));
assert(f != NULL); /* Surely infinite memory */
TAILQ_INIT(&f->cropq);
f->cursor = NULL;
farmp[fix] = f;
fprintf(stderr, "New farm (index %d) at %p\n", fix, f);
return f;
}
static void
rm_crop(struct farm *f, struct crop *c)
{
fprintf(stderr, "Del crop at %p\n", c);
TAILQ_REMOVE(&f->cropq, c, farm_cropq);
free(c);
}
static struct crop *
mk_crop(struct farm *f)
{
struct crop *c;
c = malloc(sizeof(struct crop));
assert(c != NULL);
if (f->cursor != NULL) {
/* Inherit description of current cursor */
c->describe = f->cursor->describe;
} else {
c->describe = (cid & 1) ? descr_1 : descr_2 ;
}
cid++;
TAILQ_INSERT_HEAD(&f->cropq, c, farm_cropq);
fprintf(stderr, "New crop at %p\n", c);
return c;
}
static void
rm_ufo(void)
{
if (ufo != NULL) {
fprintf(stderr, "Del UFO at %p\n", ufo);
free(ufo);
}
}
int
main(void)
{
int c;
size_t fix = 0;
#if defined(__CHERI_PURE_CAPABILITY__)
if (getenv("MALLOC_DISABLE_REVOCATION") == NULL) {
fprintf(stderr, "Ready (CHERI-RISC-V)\n");
} else {
fprintf(stderr, "Ready (CHERI-RISC-V, reduced heap safety)\n");
}
#else
fprintf(stderr, "Ready (RISC-V)\n");
#endif
while ((c = getchar()) != EOF) {
if (isspace(c))
continue;
if (('0' <= c) && (c < '0' + NFARM)) {
fix = c - '0';
fprintf(stderr, "Selected farm %zu (%p)\n", fix,
farmp[fix]);
continue;
}
struct farm *f = farmp[fix];
switch (c) {
case '!':
#if defined(__CHERI_PURE_CAPABILITY__)
malloc_revoke();
#else
fprintf(stderr, "No revocation without CHERI!\n");
#endif
break;
/* Crop management */
case 'C':
if (f != NULL)
mk_crop(f);
break;
case 'c':
if ((f != NULL) && (f->cursor != NULL))
rm_crop(f, f->cursor);
break;
case 'D':
fprintf(stderr, "FYI: Current farm is %p\n", f);
if ((f != NULL) && (f->cursor != NULL)) {
fprintf(stderr, "FYI: cursor %p\n", f->cursor);
fprintf(stderr, "FYI: cursor->describe %p\n",
f->cursor->describe);
f->cursor->describe(f->cursor);
}
break;
/* Farm management */
case 'F':
mk_farm(fix);
break;
case 'f':
rm_farm(fix);
break;
/* Cursor control */
case 'L':
if (f != NULL) {
if (f->cursor != NULL) {
f->cursor = TAILQ_PREV(f->cursor, cropq,
farm_cropq);
} else {
f->cursor = TAILQ_LAST(&f->cropq,
cropq);
}
}
fprintf(stderr, "Farm %zu cursor %p\n", fix, f->cursor);
break;
case 'R':
if (f != NULL) {
if (f->cursor != NULL) {
f->cursor = TAILQ_NEXT(f->cursor,
farm_cropq);
} else {
f->cursor = TAILQ_FIRST(&f->cropq);
}
}
fprintf(stderr, "Farm %zu cursor %p\n", fix, f->cursor);
break;
case 'Z':
if (f != NULL)
f->cursor = NULL;
break;
/* UFO control sequences */
case 'A':
if ((ufo != NULL) && (f != NULL)) {
fprintf(stderr, "UFO abduct %p\n", f->cursor);
ufo->ptrA = f->cursor;
}
break;
case 'S':
if (f != NULL) {
/* Jess's Organic Farm-to-Vtable Capability */
fprintf(stderr, "Crop sign at farm %p\n", f);
f->cursor = (void *)success;
}
break;
case 's':
if ((f != NULL) && (f->cursor != NULL)) {
char buf[sizeof(void *)];
for (size_t i = 0; i < sizeof buf; i++) {
buf[i] = getchar();
}
fprintf(stderr, "Signing crop %p\n", f->cursor);
memmove((char *)f->cursor, buf, sizeof(buf));
}
break;
case 'U':
rm_ufo();
ufo = malloc(sizeof(union ufo));
assert(ufo != NULL);
fprintf(stderr, "UFO at %p\n", ufo);
break;
case 'u':
rm_ufo();
break;
default:
fprintf(stderr, "Did not understand %x; bail!\n", c);
return 1;
}
}
return 0;
}