update cheri article

This commit is contained in:
Jack Bond-Preston 2022-11-22 00:13:56 +00:00
parent f88766280a
commit 25594e233e
Signed by: jack
GPG Key ID: 010071F1482BA852
3 changed files with 45 additions and 34 deletions

View File

@ -6,13 +6,13 @@ title: "CHERI"
## preamble ## preamble
[CHERI](https://www.cl.cam.ac.uk/research/security/ctsrd/cheri/) is an acronym for Capability Hardware Enhanced RISC Instructions. it is a security-focussed project aimed at improving memory protection at the hardware level. the project is complex and it has many potential applications. [CHERI](https://www.cl.cam.ac.uk/research/security/ctsrd/cheri/) is an acronym for Capability Hardware Enhanced RISC Instructions. it is a security-focussed project aimed at improving memory protection at the hardware level. the project is complex and it has many potential applications.
in this article I will go into some basics to give an understanding behind some changes that CHERI makes to how programs execute and are written. this will be focussed almost entirely in C, as this is where my experience lies - it is also where some of the effects of CHERI are most easily felt.this article is going to be a _very simplistic_ introduction to CHERI, and I'm going to attempt to explain the basics behind everything I cover. a basic understanding of C will be beneficial. in this article I will go into some basics to give an understanding behind some changes that CHERI makes to how programs execute and are written. this will be focussed almost entirely in C, as this is where my experience lies - it is also where some of the effects of CHERI are most easily felt.this article is going to be a _very simplistic_ introduction to CHERI, and I'm going to attempt to explain the basics behind everything I cover. a basic understanding of C will be necessary.
***note:*** [the Morello platform](https://www.arm.com/architecture/cpu/morello) is an evaluation board produced by [Arm](https://www.arm.com/) to provide a physical implementation of CHERI extending [the Arm AArch64 ISA](https://en.wikipedia.org/wiki/AArch64). I previously worked on this platform at Arm, [porting the musl C library to Morello](https://git.morello-project.org/morello/musl-libc/). implementations for CHERI that are worth looking into from a more open perspective <a href="https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-951.pdf"> are the MIPS (chapter 4) and RISC-V (chapter 5) ones</a>. Morello is the only implementation that exists in a true hard core format, afaik - but this is obviously hard to obtain so you'll just be playing around with emulators/models anyway. ***note:*** [the Morello platform](https://www.arm.com/architecture/cpu/morello) is an evaluation board produced by [Arm](https://www.arm.com/) to provide a physical implementation of CHERI extending [the Arm AArch64 ISA](https://en.wikipedia.org/wiki/AArch64). I previously worked on this platform at Arm, [porting the musl C library to Morello](https://git.morello-project.org/morello/musl-libc/). implementations for CHERI that are worth looking into from a more open perspective <a href="https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-951.pdf"> are the MIPS (chapter 4) and RISC-V (chapter 5) ones</a>. Morello is the only implementation that exists in a true hard core format, afaik - but this is obviously hard to obtain so you'll just be playing around with emulators/models anyway.
## memory safety bugs ## memory safety bugs
to first understand how CHERI tries to fix some simple issues, let's first look at some simplified examples of issues that arise when we aren't using a CHERI-based architecture. to understand how CHERI tries to fix some simple issues, we'll first look at some simplified examples of issues that arise when we aren't using a CHERI-based architecture.
### a simple memory safety bug ### a simple memory safety bug
@ -22,7 +22,7 @@ let's take a look at this C code:
{% include_relative code/membug.c %} {% include_relative code/membug.c %}
{% endhighlight %} {% endhighlight %}
now let's try using our new program: and try running the compiled output of said program:
{% highlight console %} {% highlight console %}
$ ./membug $ ./membug
@ -31,7 +31,9 @@ hello jack
my_perfect_string: what a beautiful string my_perfect_string: what a beautiful string
{% endhighlight %} {% endhighlight %}
works on my machine boss! code review +1, and merged... until our good friend <a href="https://en.wikipedia.org/wiki/Hubert_Blaine_Wolfeschlegelsteinhausenbergerdorff_Sr.">Hubert Blaine Wolfeschlegelsteinhausenbergerdorff Sr.</a> comes along. he emails me a strangeerror he's seen: works on my machine boss! code review +1, and merged...
...until our good friend <a href="https://en.wikipedia.org/wiki/Hubert_Blaine_Wolfeschlegelsteinhausenbergerdorff_Sr.">Hubert Blaine Wolfeschlegelsteinhausenbergerdorff Sr.</a> comes along. he emails me a strange error he's running into:
{% highlight console %} {% highlight console %}
$ ./membug $ ./membug
@ -40,9 +42,11 @@ hello Hubert Blaine Wolfeschlegelsteinhausenbergerdorff Sr.
my_perfect_string: hausenbergerdorff Sr. my_perfect_string: hausenbergerdorff Sr.
{% endhighlight %} {% endhighlight %}
that's not supposed to happen! his name has spilled over into our `my_perfect_string[]` array! turns out our issue is that when we use `fgets()`, we've set the second parameter, `size`, to `1000` - but our `user_name[32]` array c1593an only fit 32 characters (and the last of these should be a null terminator, so 31 usable characters). ***note:*** if you compile and run this on your machine, you may not get the same output. that's because we're invoking *undefined behaviour* here, so the compiler can kind of do whatever it wants. I'll always provide the output that demonstrates what I'm trying to show when giving examples like this. for what it's worth, I'm running `clang 10.0.0-4ubuntu1` with target `x86_64-pc-linux-gnu`. compilation options, the `Makefile`, and such are available <a href="https://github.com/jackbondpreston/jackbondpreston.github.io/tree/master/_posts/cheri/code">code subdirectory of this article's source</a>.
`fgets` fills up `user_name`, but it hasn't finished with the name yet! it doesn't care (or know) that `user_name` is full, it's just going to keep going until it finishes our user input, or reads 999 characters from standard input. and thus it keeps mindlessly writing, overwriting the memory we've used to store our precious perfect string (which happens to be immediately after `user_name`). let's take a look at the stack in GDB to see why this happens: that's not supposed to happen! his name has spilled over into our `my_perfect_string[]` array! turns out our issue is that when we use `fgets(char *str, int count, FILE *stream)`, we've set the second parameter (`size`) to `1000` - but our `user_name[32]` array can only fit 32 characters (and the last of these should be a null terminator, so 31 usable characters).
`fgets()` fills up `user_name`, but it hasn't finished with the name yet! it doesn't care (or know) that `user_name` is full, it's just going to keep going until it finishes our user input, or reads 999 characters from standard input. thus it keeps mindlessly writing, overwriting the section memory we've used to store our precious perfect string (which happens to be immediately after `user_name`). let's take a look at the stack in GDB to see how this happens:
{% highlight plaintext %} {% highlight plaintext %}
(gdb) b memdebug.c:7 (gdb) b memdebug.c:7
@ -65,21 +69,25 @@ Breakpoint 1, main () at membug.c:7
we can see our two character arrays are right next to each other on the stack (`user_name` contains some gibberish as it is not zero-initialised). we can see our two character arrays are right next to each other on the stack (`user_name` contains some gibberish as it is not zero-initialised).
***note:*** this code was compiled with `-fno-stack-protector` to reproduce this behaviour. compilers have certain techniques like this which can help protect against such attacks, but there are often ways around these by using less primitive attacks. ***note:*** this code was compiled with `-fno-stack-protector` to reproduce this behaviour. compilers have certain techniques which can help protect against such attacks, but there are often ways around these by using less primitive attacks. we are just ignoring these in this article for simplicity.
okay, it's a pretty easy fix, we just need to change the `fgets(char *s, int size, FILE *stream)` parameter `size` to `32`. okay, at least it's a pretty easy fix: we just need to change the `fgets()` parameter `size` to `32`.
***note:*** you may initially think "why not 31? don't we need to save a character for the null byte at the end?". thankfully, `fgets` does this for us. excerpt from `man fgets`: ***note:*** you may initially think "why not `31`? don't we need to save a character for the null byte at the end?". thankfully, `fgets` does this for us. excerpt from `man fgets`:
> "fgets() reads in _at most one less than size_ characters from stream and stores them into the buffer pointed to by s [...] A terminating null byte ('\0') is stored after the last character in the buffer". > `"fgets() reads in _at most one less than size_ characters from stream and stores them into the buffer pointed to by s [...] A terminating null byte ('\0') is stored after the last character in the buffer".`
this is a good question to be asking though, being careful is key when it comes to these kinds of things. this is a good question to be asking though, being careful is key when it comes to these kinds of things.
### why hardware? ### why hardware?
okay, so that's an easy fix. why are we talking about doing anything in hardware here? just write the code correctly! the issue is code gets very complex, and this is a very simplistic situation. some memory safety bugs can be incredibly complicated and go unnoticed for decades. the C language especially gives the programmer many, many opportunities to make mistakes - and it only takes one to be a problem. a lot of the software we are using these days is based on stacks upon stacks of software written in different languages, and there are going to be bugs in there. CHERI should give us some protection "for free" (it's not this simple, in actuality). okay, that wasn't too bad. why are we talking about doing anything in hardware here? just write the code correctly!
some languages (e.g. Rust) are going to offer you strong memory safety guarantees at compile-time, but that's not the topic of this article. the differences between doing this kind of protection in software or hardware (or both) is more complex than the scope of this article. in addition, CHERI's benefits are more wide in breadth than just protecting against this kind of issue. we've looked at a very simplistic situation, with no real stakes and nothing to exploit (and an unrealistically simple bug). if this bug was exploitable for malicious gain, it could already be too late by the time we found it.
memory safety problems make up the vast majority of problematic security issues. the Chromium project [found 70% of its serious security bugs were memory safety related](https://www.chromium.org/Home/chromium-security/memory-safety/) and [Microsoft found the same prevalence](https://msrc-blog.microsoft.com/2019/07/16/a-proactive-approach-to-more-secure-code/). some memory safety bugs can be incredibly complicated and go unnoticed for decades. the C language especially gives the programmer many, many opportunities to make mistakes - and it only takes one to be a problem. a lot of the software we are using these days is based on layers upon layers of software written in different languages, and there are going to be bugs in there. CHERI aims to give us some protection at a hardware level.
***Note:*** some languages (e.g. Rust) are going to offer you strong memory safety guarantees at compile-time, but I'm not going to include the discussions around this and how it compares to CHERI in this article. this article will focussed on how CHERI applies to C (and to some extent, C++ by extension).
## pointers recap ## pointers recap
@ -102,18 +110,18 @@ and on these normal architectures, this pointer generally is just a number. we c
{% highlight console %} {% highlight console %}
$ ./ptrs_as_numbers $ ./ptrs_as_numbers
*x=1234 *(7fff98640c20)=1234
*x=5678 *(7fff98640c24)=5678
*x=9999 *(7fff98640c28)=9999
{% endhighlight %} {% endhighlight %}
yikes! now, when you start messing with pointers like this, you're bound to run into a bunch of undefined behaviour. but C programmers write undefined behaviour all the time, and my computer executes this program fine without complaining at all. doesn't it feel a bit weird that we can take a pointer to `arr[0]` and modify it to load `secret`? they're not even part of the same array... yikes! now, when you start messing with pointers like this, you're bound to run into a bunch of undefined behaviour. but C programmers write undefined behaviour all the time (and not always by accident), and my computer executes this program fine without complaining at all. doesn't it feel a bit weird that we can take a pointer to `arr[0]` and modify it to load `secret`? they're not even part of the same array...
## introducting capabilities ## introducting capabilities
CHERI introduces capabilities, which can be thought of as an extension to pointers. they still store an address of something we care about, but they have extra information too! in a 64-bit system, a pointer would typically be a 64-bit value (as dicussed previously). the corresponding capability in a CHERI platform is 128 bits (or 129 bits if you look at it a certain way, more about that later...). CHERI introduces capabilities, which can be thought of as an extension to pointers. they still store an address of something we care about, but they have extra information too! in a 64-bit system, a pointer would typically be a 64-bit value (as dicussed previously). the corresponding capability in a CHERI platform is 128 bits (or 129 bits if you look at it a certain way, more about that later).
as you might have guessed, this "extra information" takes up 64 bits of the capability. bits are assigned to three key pieces of metadata: *bounds*, *permissions*, and *object type*. there is also an additional 1-bit _tag_ which is stored out-of-band: it is not a 129-bit value - instead each 128-bit capability can be thought of as being associated with a 1-bit validity tag. the architecture manages this. the diagram below is provided as a rough overview of this. note that it is not to scale. as you might have guessed, this "extra information" takes up 64 bits of the capability. bits are assigned to three key pieces of metadata: *bounds*, *permissions*, and *object type*. there is also an additional 1-bit _tag_ which is stored out-of-band: it is not a 129-bit value - instead each 128-bit capability can be thought of as being associated with a 1-bit validity tag. the architecture manages this association for us. the diagram below is provided as a rough overview of this. note that it is not to scale.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 314"><defs><style>.prefix__c{fill:none;stroke:#fcfcfc;stroke-linecap:square;stroke-miterlimit:10;stroke-width:3px}.prefix__f,.prefix__g{fill:#fcfcfc}.prefix__f{font-family:Source Code Pro;font-size:20px}</style></defs><g id="prefix__a"><path fill="#0c1114" d="M0 0h1920v314H0z"/><text transform="translate(101.86 232.41)" font-family="Source Code Pro" font-weight="700" fill="#fcfcfc" font-size="24"><tspan x="0" y="0">int *x (capability)</tspan></text><text transform="translate(1205.97 232.1)" font-family="Source Code Pro" fill="#fcfcfc" font-size="24"><tspan x="0" y="0">0x0000010000000004</tspan></text><path class="prefix__c" d="M1016 261v18.5M1656 261v18.5M1016 279.5h640"/><text class="prefix__f" transform="translate(1293.78 296.33)"><tspan x="0" y="0">address</tspan></text><path class="prefix__c" d="M700 191.5V173M1020 191.5V173M700 173h320"/><text class="prefix__f" transform="translate(823.78 167.74)"><tspan x="0" y="0">bounds</tspan></text><path class="prefix__c" d="M554 260.34v18.5M704 260.34v18.5M554 278.84h150"/><text class="prefix__f" transform="translate(562.78 295.68)"><tspan x="0" y="0">object type</tspan></text><g><path class="prefix__c" d="M391.89 191.56v-18.5M541.89 191.56v-18.5M391.89 173.06h150"/></g><text class="prefix__f" transform="translate(400.67 167.8)"><tspan x="0" y="0">permissions</tspan></text><text class="prefix__f" transform="translate(304.67 31.07)"><tspan x="0" y="0">tag (out-of-band)</tspan></text><g><path class="prefix__c" d="M391.33 55.92v-18.5M421.33 55.92v-18.5M391.33 37.42h30"/></g></g><g id="prefix__b"><path class="prefix__g" d="M1651.66 205.93v40h-632v-40h632m4-4h-640v48h640v-48z"/><path class="prefix__g" d="M1016 206v40H704v-40h312m4-4H700v48h320v-48z"/><path class="prefix__g" d="M700 206v40H558v-40h142m4-4H554v48h150v-48z"/><path class="prefix__g" d="M554 206v40h-12v-40h12m4-4h-20v48h20v-48z"/><path class="prefix__g" d="M538 206v40H396v-40h142m4-4H392v48h150v-48zM418.5 70v40h-22V70h22m4-4h-30v48h30V66z"/></g></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 314"><defs><style>.prefix__c{fill:none;stroke:#fcfcfc;stroke-linecap:square;stroke-miterlimit:10;stroke-width:3px}.prefix__f,.prefix__g{fill:#fcfcfc}.prefix__f{font-family:Source Code Pro;font-size:20px}</style></defs><g id="prefix__a"><path fill="#0c1114" d="M0 0h1920v314H0z"/><text transform="translate(101.86 232.41)" font-family="Source Code Pro" font-weight="700" fill="#fcfcfc" font-size="24"><tspan x="0" y="0">int *x (capability)</tspan></text><text transform="translate(1205.97 232.1)" font-family="Source Code Pro" fill="#fcfcfc" font-size="24"><tspan x="0" y="0">0x0000010000000004</tspan></text><path class="prefix__c" d="M1016 261v18.5M1656 261v18.5M1016 279.5h640"/><text class="prefix__f" transform="translate(1293.78 296.33)"><tspan x="0" y="0">address</tspan></text><path class="prefix__c" d="M700 191.5V173M1020 191.5V173M700 173h320"/><text class="prefix__f" transform="translate(823.78 167.74)"><tspan x="0" y="0">bounds</tspan></text><path class="prefix__c" d="M554 260.34v18.5M704 260.34v18.5M554 278.84h150"/><text class="prefix__f" transform="translate(562.78 295.68)"><tspan x="0" y="0">object type</tspan></text><g><path class="prefix__c" d="M391.89 191.56v-18.5M541.89 191.56v-18.5M391.89 173.06h150"/></g><text class="prefix__f" transform="translate(400.67 167.8)"><tspan x="0" y="0">permissions</tspan></text><text class="prefix__f" transform="translate(304.67 31.07)"><tspan x="0" y="0">tag (out-of-band)</tspan></text><g><path class="prefix__c" d="M391.33 55.92v-18.5M421.33 55.92v-18.5M391.33 37.42h30"/></g></g><g id="prefix__b"><path class="prefix__g" d="M1651.66 205.93v40h-632v-40h632m4-4h-640v48h640v-48z"/><path class="prefix__g" d="M1016 206v40H704v-40h312m4-4H700v48h320v-48z"/><path class="prefix__g" d="M700 206v40H558v-40h142m4-4H554v48h150v-48z"/><path class="prefix__g" d="M554 206v40h-12v-40h12m4-4h-20v48h20v-48z"/><path class="prefix__g" d="M538 206v40H396v-40h142m4-4H392v48h150v-48zM418.5 70v40h-22V70h22m4-4h-30v48h30V66z"/></g></svg>
@ -123,7 +131,7 @@ I am mostly going to focus on _bounds_ in this article, as it is not too difficu
can you imagine how we can use bounds to prevent our previous memory safety bug from occurring? the key is that we can set the bounds on the capability pointing to `user_name` which we pass to `fgets`, such that the capability may only access the contents of the array. this means that when `fgets` tries to write past the end of the `user_name` array, the processor will throw a *capability fault*, and execution of our program will cease. can you imagine how we can use bounds to prevent our previous memory safety bug from occurring? the key is that we can set the bounds on the capability pointing to `user_name` which we pass to `fgets`, such that the capability may only access the contents of the array. this means that when `fgets` tries to write past the end of the `user_name` array, the processor will throw a *capability fault*, and execution of our program will cease.
the idea behind CHERI is that we don't have to set up these bounds ourselves. this is something the compiler can generate code for. the compiler knows that the `user_name` array has a length of `32`, and can set the bounds accordingly on capabilities created that point to it. let's try it... the idea behind CHERI is that we as the C programmer don't have to set up these bounds ourselves most of the time---this is something the compiler can generate code for. the compiler knows that the `user_name` array has a length of `32`, and can set the bounds accordingly on capabilities created that point to it. let's try it...
## playing with CHERI RISC-V ## playing with CHERI RISC-V
@ -142,11 +150,13 @@ login: root
root@cheribsd-riscv64-purecap:~ # root@cheribsd-riscv64-purecap:~ #
{% endhighlight %} {% endhighlight %}
now we have our shell inside our CheriBSD emulated platform, we can start to try things out. let's compile our `membug` program again, this time with the toolchain targetting CheriBSD RISC-V - this will have been built as part of the dependencies already. once it's built, we can `scp` it over to the CheriBSD filesystem, as we set up the SSH forwarding port to now we have our shell inside our CheriBSD emulated platform, we can start to try things out. let's compile our `membug` program again, this time with the toolchain targetting CheriBSD RISC-V - this will have been built as part of the dependencies already.
`1111`.
once our `membug-cheribsd` executable is built, we can `scp` it over to the CheriBSD filesystem. remember, we set up the SSH forwarding port to `1111`.
from a terminal on your host machine:
{% highlight console %} {% highlight console %}
# on a separate terminal on your host machine
$ ~/cheri/output/sdk/utils/cheribsd-riscv64-purecap-clang membug.c -Wall -g -fno-stack-protector -o membug-cheribsd $ ~/cheri/output/sdk/utils/cheribsd-riscv64-purecap-clang membug.c -Wall -g -fno-stack-protector -o membug-cheribsd
$ scp -P 2222 ./membug-cheribsd root@localhost:~/ $ scp -P 2222 ./membug-cheribsd root@localhost:~/
{% endhighlight %} {% endhighlight %}

View File

@ -1,4 +1,4 @@
CFLAGS += -Wall -g -fno-stack-protector CFLAGS += -Wall -g -fno-stack-protector -O0
CC ?= clang CC ?= clang
PURECAP_CC ?= ~/cheri/output/sdk/utils/cheribsd-riscv64-purecap-clang PURECAP_CC ?= ~/cheri/output/sdk/utils/cheribsd-riscv64-purecap-clang

View File

@ -2,20 +2,21 @@
int main() { int main() {
int magic = 9999; int magic = 9999;
(void)magic; (void)magic; // suppress unused warning
int arr[] = { 1234, 5678 }; int arr[] = { 1234, 5678 };
int *x = &(arr[0]); // x is a pointer to first element of arr int *x = &(arr[0]); // x is a pointer to first element of arr
printf("*x=%d\n", *x); printf("*(%lx)=%d\n", (size_t) x, *x);
unsigned long x_addr = (size_t) x; // we're going to assume size_t = unsigned long here unsigned long x_addr = (size_t) x; // we're going to assume size_t = unsigned long here
x_addr += 4; // sizeof(int) == 4 x_addr += 4; // sizeof(int) == 4
x = (int *) x_addr; x = (int *) x_addr;
printf("*x=%d\n", *x); printf("*(%lx)=%d\n", (size_t) x, *x);
x_addr += 4; x_addr += 4;
x = (int *) x_addr; x = (int *) x_addr;
printf("*x=%d\n", *x); printf("*(%lx)=%d\n", (size_t) x, *x);
return 0; return 0;
} }