Answers - Exercise an inter-stack-object buffer overflow
-
Expected output:
# ./buffer-overflow-stack-baseline upper = 0x80d879d0, lower = 0x80d879c0, diff = 10 upper[0] = a upper[0] = b # ./buffer-overflow-stack-cheri upper = 0x3fffdfff50, lower = 0x3fffdfff40, diff = 10 upper[0] = a In-address space security exception
-
An example session of
gdb-run.sh ./buffer-overflow-stack-cheri
on CHERI-RISC-V:Reading symbols from ./buffer-overflow-stack-cheri... Starting program: /mnt/buffer-overflow-stack-cheri upper = 0x3fffdfff50, lower = 0x3fffdfff40, diff = 10 upper[0] = a Program received signal SIGPROT, CHERI protection violation Capability bounds fault caused by register ca0. 0x0000000000101cf0 in write_buf (buf=<optimized out>, ix=<optimized out>) at buffer-overflow-stack.c:13 13 buf[ix] = 'b'; Thread 1 (LWP 100055 of process 829): #0 0x0000000000101cf0 in write_buf (buf=<optimized out>, ix=<optimized out>) at buffer-overflow-stack.c:13 #1 0x0000000000101d7a in main () at buffer-overflow-stack.c:31 (gdb) disass Dump of assembler code for function write_buf: 0x0000000000101ce8 <+0>: cincoffset ca0,ca0,a1 0x0000000000101cec <+4>: li a1,98 => 0x0000000000101cf0 <+8>: csb a1,0(ca0) 0x0000000000101cf4 <+12>: cret End of assembler dump.
Asking
gdb
about the registers withinfo registers
and focusing on the ones involved here, we seea0 0x3fffdfff50 274875809616 a1 0x62 98 ca0 0xd17d000007d5bf440000003fffdfff50 0x3fffdfff50 [rwRW,0x3fffdfff40-0x3fffdfff50] ca1 0x62 0x62
The capability in
ca0
, which is a pointer into thelower
buffer, has been taken beyond the end of the allocation, as out of bounds store has been attempted (Capability bounds fault
).But where did those bounds originate? Heading
up
a stack frame anddisass
embling, we see (eliding irrelevant instructions):(gdb) up #1 0x0000000000101d7a in main () at buffer-overflow-stack.c:31 31 write_buf(lower, sizeof(lower)); (gdb) disass Dump of assembler code for function main: 0x0000000000101cf8 <+0>: cincoffset csp,csp,-144 0x0000000000101d14 <+28>: cincoffset ca0,csp,48 0x0000000000101d18 <+32>: csetbounds cs0,ca0,16 0x0000000000101d6c <+116>: li a1,16 0x0000000000101d6e <+118>: cmove ca0,cs0 0x0000000000101d72 <+122>: auipcc cra,0x0 0x0000000000101d76 <+126>: cjalr -138(cra) => 0x0000000000101d7a <+130>: clbu a0,0(cs1)
The compiler has arranged for
main
to allocate 144 bytes on the stack by decrementing the capability stack pointer register (csp
) by 144 bytes. Further, the compiler has placedlower
48 bytes up into that allocation:ca0
is made to point at its lowest address and then the pointer tolower
is materialized incs0
by bounding the capability inca0
to be 16 (sizeof(lower)
) bytes long. This capability is passed towrite_buf
inca0
. -
The code for
write_buf
function is only slightly changed. On RISC-V it compiles to<write_buf>: add a0, a0, a1 addi a1, zero, 98 sb a1, 0(a0) ret
while on CHERI-RISC-V, it is
<write_buf>: cincoffset ca0, ca0, a1 addi a1, zero, 98 csb a1, 0(ca0) cret
In both cases, it amounts to displacing the pointer passed in
a0
(resp.ca0
) by the offset passed ina1
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, thecsb
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
inmain
, and the derivation of the capability to thelower
buffer, above. In the baseline version, the corresponding instructions are shown as(gdb) disass main Dump of assembler code for function main: 0x0000000000011b44 <+0>: addi sp,sp,-48 0x0000000000011b8a <+70>: mv a0,sp 0x0000000000011b8c <+72>: li a1,16 0x0000000000011b8e <+74>: auipc ra,0x0 0x0000000000011b92 <+78>: jalr -86(ra) # 0x11b38 <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 thelower
buffer towrite_buf
, the program simply copies the stack pointer register (an integer register, holding an address) to the argument registera0
. The subsequent address arithmetic derives an address out of bounds, clobbering a byte of theupper
register.