Answers - Exercise an inter-stack-object buffer overflow

  1. Expected output:
# run_qemu ./buffer-overflow-stack-baseline
upper = 0000003FFFFFEFA0, lower = 0000003FFFFFEF90, diff = 10
upper[0] = a
upper[0] = b
# run_qemu ./buffer-overflow-stack-cheri
upper = 00000000000000000000003FFFFFEF30, lower = 00000000000000000000003FFFFFEF20, diff = 10
upper[0] = a
MON|ERROR: received message 0x00000006  badge: 0x0000000000000001  tcb cap: 0x8000000000000008
MON|ERROR: faulting PD: buffer-overflow-stack-cheri

An example of the Monitor's output for buffer-overflow-stack-cheri` on CHERI-RISC-V:

 MON|ERROR: received message 0x00000006  badge: 0x0000000000000001  tcb cap: 0x8000000000000008
 MON|ERROR: faulting PD: buffer-overflow-stack-cheri
 Registers:
 ddc : 0x0
 pcc : 0x200060 [rxCM1111,0x200000-0x204000] (capmode)
 cra : 0x2000ea [rxCM1111,0x200000-0x204000] (capmode) (sentry)
 csp : 0x3fffffeef0 [rwCM1111,0x3fffffe000-0x3ffffff000]
 cgp : 0x0
 cs0 : 0x3fffffef20 [rwCM1111,0x3fffffef20-0x3fffffef30]
 cs1 : 0x202693 [rCM1111,0x202693-0x2026a2]
 cs2 : 0x0
 cs3 : 0x203000 [rwCM1111,0x203000-0x203010]
 cs4 : 0x0
 cs5 : 0x0
 cs6 : 0x0
 cs7 : 0x0
 cs8 : 0x0
 cs9 : 0x0
 cs10 : 0x0
 cs11 : 0x0
 ca0 : 0x3fffffef30 [rwCM1111,0x3fffffef20-0x3fffffef30]
 ca1 : 0x62
 ca2 : 0xd
 ca3 : 0xffffffffffffffff
 ca4 : 0x0
 ca5 : 0x0
 ca6 : 0x0
 ca7 : 0xfffffffffffffff4
 ct0 : 0x0
 ct1 : 0x201d46 [rxCM1111,0x200000-0x202cf0] (capmode)
 ct2 : 0x0
 ct3 : 0x0
 ct4 : 0x0
 ct5 : 0x0
 ct6 : 0x0
 ctp : 0x0
 MON|ERROR: CHERI Security Violation: ip=0x0000000000200060  fault_addr=0x0000003fffffef30  fsr=0x0000000000000814  (data fault)
 MON|ERROR: description of fault: Bounds violation
 MON|ERROR: CHERI fault type: CHERI data fault due to load, store or AMO
 <<seL4(CPU 0) [receiveIPC/142 T0xffffffc0fffe6400 "rootserver" @8a000460]: Reply object already has unexecuted reply!>

Using GDB or llvm-objdump to see what instructions at the faulting PC (ip=0x000200060), we see:

;       buf[ix] = 'b';
200060: 23 00 b5 00   sb      a1, 0(ca0)

If we investigate the content of ca0 (either using GDB's info reg $ca0 or from the register dump), we can see it does have something like:

ca0 : 0x3fffffef30 [rwCM1111,0x3fffffef20-0x3fffffef30]

The capability in ca0, which is a pointer into the lower buffer, has been taken beyond the end of the allocation, as out of bounds store has been attempted (Bounds violation).

But where did those bounds originate? Heading up a stack frame and disassembling, we see (eliding irrelevant instructions):

	(gdb) up
 #1  0x00000000002000ea in init () at buffer-overflow-stack.c:31
	31              write_buf(lower, sizeof(lower));
	(gdb) disass
	Dump of assembler code for function init:
	   0x0000000000200066 <+0>:     c.addi16sp      csp,-128

	   0x0000000000200074 <+14>:    c.addi4spn      a0,csp,48
	   0x0000000000200076 <+16>:    scbndsi cs0,ca0,16

	   0x00000000002000dc <+118>:   li      a1,16
	   0x00000000002000de <+120>:   mv      ca0,cs0
	   0x00000000002000e2 <+124>:   auipcc  cra,0x0
	   0x00000000002000e6 <+128>:   jalr    -138(cra)
	=> 0x00000000002000ea <+132>:   lbu     a0,64(csp)

The compiler has arranged for init to allocate 128 bytes on the stack by decrementing the capability stack pointer register (csp) by 128 bytes. Further, the compiler has placed lower 48 bytes up into that allocation: ca0 is made to point at its lowest address and then the pointer to lower is materialized in cs0 by bounding the capability in ca0 to be 16 (sizeof(lower)) bytes long. This capability is passed to write_buf in ca0.

The code for write_buf function is only slightly changed. On RISC-V it compiles to

 20004c <write_buf>:
 20004c: 2e 95         add     a0, a0, a1
 20004e: 93 05 20 06   li      a1, 98
 200052: 23 00 b5 00   sb      a1, 0(a0)
 200056: 82 80         ret

while on CHERI-RISC-V, it is

 200058 <write_buf>:
 200058: 33 05 b5 0c   cadd    ca0, ca0, a1
 20005c: 93 05 20 06   li      a1, 98
 200060: 23 00 b5 00   sb      a1, 0(ca0)
 200064: 82 80         ret

In both cases, it amounts to displacing the pointer passed in a0 (resp. ca0) by the offset passed in a1 and then performing a store-byte instruction before returning. In the baseline case, the store-byte takes an integer address for its store, while in the CHERI case, the store-byte takes a capability authorizing the store. There are no conditional branches or overt bounds checks in the CHERI instruction stream; rather, the sb instruction itself enforces the requirement for authority to write to memory, in the shape of a valid, in-bounds capability.

We have already seen the CHERI program's call site to write_buf in init, and the derivation of the capability to the lower buffer, above. In the baseline version, the corresponding instructions are shown as

 Breakpoint 1, init () at buffer-overflow-stack.c:23
 23                  upper, lower, (size_t)(upper - lower));
 (gdb) disassemble init
 Dump of assembler code for function init:
    0x0000000000200058 <+0>:     addi    sp,sp,-48

    0x00000000002000c0 <+104>:   mv      a0,sp
    0x00000000002000c2 <+106>:   li      a1,16
    0x00000000002000c4 <+108>:   auipc   ra,0x0
    0x00000000002000c8 <+112>:   jalr    -120(ra) # 0x20004c <write_buf>

Here, the compiler has reserved only 48 bytes of stack space and has placed the lower buffer at the lowest bytes of this reservation. Thus, to pass a pointer to the lower buffer to write_buf, the program simply copies the stack pointer register (an integer register, holding an address) to the argument register a0. The subsequent address arithmetic derives an address out of bounds, clobbering a byte of the upper register.