Hardening via the runtime ========================= Hardening against exploits that rely on memory corruption etc. Even though SLUL is meant to be a safe language it cold still have bugs (e.g. miscompilation). Hardening vtables-like structures? ---------------------------------- Two possiblities here: 1. Keep pointers in a separate data area, which can be mprotect()'ed as read-only and then mimmutable()'ed / mseal()'ed. 2. Keep *jump instructions* in a separate area, which can be mprotect()'ed as execute-only (even read disallowed) and then mimmutable()'ed / mseal()'ed. Pros: Makes it harder to obtain the address of the runtime. (In particular if the runtime clears all pointers from stack before returning. Or maybe the stack should always be zero'ed out before returning from any function call.) Cons: Requires the platform to allow generating executable code at runtime. Zero out registers and stack on function-call, return, OR immediately --------------------------------------------------------------------- When a function is called or gets called "seldom enough" it can be a good idea to zero out stack and registers as much as possible. OR, perhaps better (and more secure): Zero out anything that becomes "dead": Both registers, stack variables, heap variables, and global variables. Registers can contain both variables and compiler temporaries. (As an optimization, this could be done for all addresses and for all memory/non-register variables) The following can NOT be zero'ed out when calling a function: * Function parameter registers that are used. * Caller-saved registers that in in use. * Stack variables that are in use (including passing them to the function). The following can NOT be zero'ed out when returning: * Return registers that are in use. * Return values that are passed on the stack. * Any caller-saved registers. See also "Setting non-return argument registers to known garbage values". Unmap parts of RTL before calling main() ---------------------------------------- Could unmap the following before the RTL transfers control to the application: * RTL initialization code. * `giveme` constructors. * Dynamic library loading code (if dynamic loading is handled by the runtime) * Dynamically-dead-code (things that are not needed for the given `giveme`s). Note that the code to jump to the final entry point is always necessary, and so is (almost always) the exit function. Probably it shouldn't simply be unmapped, but rather mapped with some guard pages or similar (and those could be mimmutable/mseal'ed). This gives two benefits: 1. It removes some (out of many) possible gadges in the runtime. 2. It possibly free's up a few kilobytes of memory (not guaranteed to make any difference on virtual memory systems). There's also a downside: It might cause more overhead in the kernel to have two small mappings rather than a small one. Turn EFAULT into trap / abort() / segfault ------------------------------------------- EFAULT can be used to probe for accessbility of pages. I.e. you can scan memory to locate gadgets and/or certain libraries / functions / etc. EFAULT should never happen in normal programs written in safe programming language. Pre-map memory and disable mmap ------------------------------- Pre-map all memory that might be needed, e.g. gigabytes. On some platforms (mainly shared-kernel VPS platforms) having a page mapped but not populated yet will count against the memory quota (a.k.a. "RAM" size of the VPS). So the runtime would have to detect when should be done or not. Downsides: * Special-casing for different platforms. * There's probably *some* cost of having pages mapped, even if not populated yet. Even if the kernel creates page-table entries lazily, there might still be some kind of data structure in the kernel that occupies some space. * Some exploits become *easier* because some out-of-bounds no longer trigger a segmentation fault. * Similarly, some out-of-bounds bugs may go undetected. See also the "seccomp ruleset" section. seccomp ruleset --------------- Install a BPF program at startup to disallow unexpected calls. For example, it could: * allow-list only known-to-be-called syscalls (based on the `givemes` in the application). E.g. execve etc. might not be needed. * allow-list flags/parameters in syscalls. E.g. for mmap, PROT_EXEC might not be needed. * allow only system calls originating from specific locations in the runtime library and/or the vDSO. - IF the vDSO is allowed to make syscalls, then the application code shouldn't be allowed to know the address of the vDSO. (but it can learn this via /proc...) * kill the process if it tries to EFAULT-probe memory via e.g. write() or other syscalls that read memory from userspace. - there seems to be no way to invoke bpf rules on syscall returns - but checking pointer arguments might work (of all allowed syscalls) - some syscalls might be impossible, though, if they use more than 6 args. if it is too hard (or slow) to do with seccomp, would it be enough to do it in the runtime library? In some cases, it might even be possible to use SECCOMP_SET_MODE_STRICT. Lock-out on segmentation fault / trap / assertion failure / etc. ---------------------------------------------------------------- grsecurity has this for the kernel. Maybe it can be done in userspace, in servers, as well? Should then block the IP (or maybe subnet in case of rotating IPs), and (maybe) kill all existing processes that are serving that IP (or maybe even those that *have* recently served it). This would require some sticky-ness of each IP / subnet, or it would open up for DoS attacks. Also, there's the case when all (or most) requests come from a single IP (e.g. a reverse proxy / load balancer). That needs to be taken into account as well. Runtime/Software-based Pointer Authentication (very slow?) ---------------------------------------------------------- Jump to an address in execute-only memory in the runtime-library. At this address, there's a jump to a function at random location. That function can then perform the operation. There has to be at least two such functions (for code pointers) and maybe more for data pointers: * Call authenticated code pointer * Make authenticated code pointer * Dereference authenticated data pointer with offset and read * Dereference authenticated data pointer with offset and write * Dereference and copy between source/target authenticated data pointers with offsets * Dereference and compare authenticated data pointers with offsets (side-channel free) * Dereference and memset authenticated data pointer with offset * Create authenticated data pointer from stack address * Create authenticated data pointer from .rodata address * Advance authenticated data pointer * Compare authenticated data pointers with offsets for equality * Compare authenticated data pointers with offsets for ordering (less, equal, greater) * Navigate in authenticated data pointer with offset (a = *b->f) Note: This leads to a separate, incompatible, ABI. Runtime/Software-based indexed off-side array pointer storage (very slow?) -------------------------------------------------------------------------- Instead of normal pointers, have indexes into an array that is only accessible by the runtime (or at least only writeable by the runtime). The array would contain elements like this: (address of pointer itself, real pointer value) Support the operations in the previous section. Read-only operations might be possible to inline safely. The total size of a pointer becomes rather large: (32+2*W) bits. 32 bits for the index into the array 2*W per array element. (W is the machine pointer word size). This approach could be extended with hashed type information, with the following tuples in the array: (address of pointer itself, type-hash, real pointer value) Note that there might have to be two areas, one for code pointers and another for data pointers. Also, note that one index (e.g. 0) for the NULL value. Note: This design leads to a separate, incompatible, ABI. Runtime/Software-based tree with pointer/object info (extremely slow?) ---------------------------------------------------------------------- In a restricted area in memory, accessible only by the runtime, store a tree of: address of pointer itself -> real pointer value When reading a pointer value, check that the value in the map and the value of the pointer match. When writing a pointer value, update the map accordingly. This technique could be extended to type information also, e.g. address of object -> type and size of object Protected runtime storage for important bits -------------------------------------------- Important flags, e.g. "isAdmin", could be shadowed by the runtime, in a in a protected area that is only accessible by the runtime. There could also be set-only "fuse-like" bits. Internally, this could be a tree structure. The (internal) API could be: slulrt__set_bit(id, value) slulrt__clear_bit(id) slulrt__require_bit(id) slulrt__set_oneway_bit(id) slulrt__require_no_oneway_bit(id) Setting/checking a bit with conflicting APIs (bit vs oneway_bit) should trigger a trap. The `id` would just be some kind of compile-time hash of the attribute. The authentication code path could be: login username password Runtime.set_bit "is_admin" is_admin A following authorization path might be: if is_admin Runtime.require_bit "is_admin" add_user new_user_name end This could protect against some specific data-only attacks. Setting non-return argument registers to known garbage values ------------------------------------------------------------- As an anti-ROP (return-oriented programming) measure, one could set all argument registers (except the return register(s)) to garbage values just before the return instruction. This will also clear out any sensitive data in those registers, as a side-effect. mox x0, ... mov x1, -123 mov x2, -123 ... ret In order to harden against JOP (jump-oriented programming), the registers could also be "junked" before any indirect jump or indirect call. This "register junking" could also happen when an instruction contains a return (or jump?) instruction, e.g. a 0xC2 or 0xC3 byte on x86. Checking that padding bytes/bits are zero ----------------------------------------- There are many places where there are alignment padding bytes. If those are always initialized to zero (they probably should be), then it is simple to check that they are still zero at "important places" in the program (e.g. before a return from a function). Similarly, pointers could be validated to be aligned (zero bits at end) and not have low non-zero values (e.g. 1..65535), and if non-nullable, not be zero. Disadvantages: * Calling from C code is trickier, because everything must be initially zero-initialized (or padding areas zeroed out). * Some out-of-bounds accesses that would previously be detected as an uninitialized value in Valgrind will no longer be detected. (Due to padding areas now appearing to be properly initialized). Special calling convention for "risky" functions ------------------------------------------------ Low-level RTL functions that do risky stuff could require special register values, for example this on aarch64: x1 = 0xFF00007F + this contains several special bytes, and requires multiple instructions to produce. Also, it requires a "func_id" to prevent "stealing" it from another risky function. x2 = + x1 this isn't constant, which makes it harder to produce. "Risky" function could include e.g. mapping memory, creating files, establishing network connections, executing processes, or perhaps anything OS or native-code related. Risky functions could additionally be mapped into a separate memory mapping (with a random address) to prevent calling them directly. If all code is execute-only (no read permission) and there are no leaks (e.g. via registers), then it will be hard to figure out the address to the "risky function area". Restrict writes within an arena ------------------------------- If an arena only has access to it's own data, and the virtual memory of the arena can be reserved beforehand (size and contiguity), then all write operations could be bounds-checked. But this is tricky to do: 1. Might not know whether to bounds-check at compile time. 2. Other code might be called, that doesn't know about the write restriction. 3. Setting up a separate memory map might require spawning a full new process. - But it seems that aarch64 has some kind of "permission overlays"? 4. With a separate memory map, (2) would cause segfaults, if enforced strictly. - This would also make I/O or other system functions trickier. Aside: Enforcing "not-worsened" constant-time for `wrapping` integers --------------------------------------------------------------------- Wrapping integers are intended to be used for e.g. cryptographic operations. When a variable is of a `wrapping` type: * If it's is not used in any branches, enforce (in the code generator) that it is never used in branches in the generated machine code. * If it's not used in any data-dependent lookups etc, used the "DIT" subset on aarch64 (and equivalent subsets on other CPU ISAs) Or perhaps there should be a separate type that enforces these two?